mirror of https://github.com/aseprite/aseprite.git
Polish layout handling
@dacap's notes: A description of the included changes: * Improve UX auto-saving layouts when docks are modified, and new 'X' icon to delete layouts (or reset the 'Default' layout). * Remove old timeline position controls (Left/Right/Bottom buttons) from the Timeline configuration and from the layout selector * Add support to drag and drop docks to other sides with real-time feedback using a semi-transparent UILayer * Add a context menu w/right-click to dock the widget at the supported sides without drag-and-drop Some review comments in https://github.com/dacap/aseprite/pull/2
This commit is contained in:
parent
f556052fc6
commit
c445075a79
|
@ -1245,8 +1245,19 @@ default_new_layer_name = New Layer
|
||||||
|
|
||||||
[new_layout]
|
[new_layout]
|
||||||
title = New Workspace Layout
|
title = New Workspace Layout
|
||||||
|
base = Base:
|
||||||
name = Name:
|
name = Name:
|
||||||
default_name = User Layout {}
|
modified = {} (Modified)
|
||||||
|
deleting_layout = Deleting Layout
|
||||||
|
deleting_layout_confirmation = Are you sure you want to delete the layout '{}'?
|
||||||
|
restoring_layout = "Restoring Layout"
|
||||||
|
restoring_layout_confirmation = Are you sure you want to restore the {} layout?"
|
||||||
|
|
||||||
|
[dock]
|
||||||
|
left = Dock Left
|
||||||
|
right = Dock Right
|
||||||
|
top = Dock Top
|
||||||
|
bottom = Dock Bottom
|
||||||
|
|
||||||
[news_listbox]
|
[news_listbox]
|
||||||
more = More...
|
more = More...
|
||||||
|
|
|
@ -3,15 +3,21 @@
|
||||||
<gui>
|
<gui>
|
||||||
<window id="new_layout" text="@.title">
|
<window id="new_layout" text="@.title">
|
||||||
<vbox>
|
<vbox>
|
||||||
<hbox>
|
<grid columns="2">
|
||||||
|
<label text="@.base" />
|
||||||
|
<combobox id="base" expansive="true">
|
||||||
|
<listitem text="@main_window.default_layout" value="_default_original_" />
|
||||||
|
<listitem text="@main_window.mirrored_default_layout" value="_mirrored_default_original_" />
|
||||||
|
</combobox>
|
||||||
|
|
||||||
<label text="@.name" />
|
<label text="@.name" />
|
||||||
<vbox id="name_placeholder" expansive="true" />
|
<entry maxsize="128" id="name" magnet="true" expansive="true" />
|
||||||
</hbox>
|
</grid>
|
||||||
|
|
||||||
<separator horizontal="true" />
|
<separator horizontal="true" />
|
||||||
|
|
||||||
<hbox homogeneous="true" cell_align="right">
|
<hbox homogeneous="true" cell_align="right">
|
||||||
<button text="@general.ok" closewindow="true" id="ok" magnet="true" minwidth="60" />
|
<button text="@general.ok" closewindow="true" id="ok" disabled="true" minwidth="60" magnet="true" />
|
||||||
<button text="@general.cancel" closewindow="true" />
|
<button text="@general.cancel" closewindow="true" />
|
||||||
</hbox>
|
</hbox>
|
||||||
</vbox>
|
</vbox>
|
||||||
|
|
|
@ -3,38 +3,25 @@
|
||||||
<!-- Copyright (C) 2014-2018 by David Capello -->
|
<!-- Copyright (C) 2014-2018 by David Capello -->
|
||||||
<gui>
|
<gui>
|
||||||
<vbox id="timeline_conf">
|
<vbox id="timeline_conf">
|
||||||
<hbox>
|
<vbox>
|
||||||
<vbox>
|
<separator text="@.frame_header" left="true" horizontal="true" />
|
||||||
<separator cell_hspan="2" text="@.position" left="true" horizontal="true" />
|
<hbox>
|
||||||
<hbox>
|
<label text="@.first_frame" />
|
||||||
<buttonset columns="2" id="position">
|
<expr id="first_frame" />
|
||||||
<item text="@.left" />
|
</hbox>
|
||||||
<item text="@.right" />
|
<hbox>
|
||||||
<item text="@.bottom" hspan="2" />
|
<check id="thumb_enabled" text="@.thumbnails" horizontal="true" />
|
||||||
</buttonset>
|
<separator id="thumb_h_separator" horizontal="true" expansive="true" />
|
||||||
</hbox>
|
</hbox>
|
||||||
</vbox>
|
<grid columns="2" id="thumb_box">
|
||||||
<vbox>
|
<label text="@.thumbnail_size" />
|
||||||
<separator text="@.frame_header" left="true" horizontal="true" />
|
<slider min="1" max="10" id="zoom" cell_align="horizontal" width="128" />
|
||||||
<hbox>
|
|
||||||
<label text="@.first_frame" />
|
|
||||||
<expr id="first_frame" />
|
|
||||||
</hbox>
|
|
||||||
|
|
||||||
<hbox>
|
<check id="thumb_overlay_enabled" text="@.overlay_size"/>
|
||||||
<check id="thumb_enabled" text="@.thumbnails" horizontal="true" />
|
<slider min="2" max="10" id="thumb_overlay_size" cell_align="horizontal" width="128" />
|
||||||
<separator id="thumb_h_separator" horizontal="true" expansive="true" />
|
<check id="thumb_scale_up_to_fit" text="@.scale_up_to_fit" cell_hspan="2" />
|
||||||
</hbox>
|
</grid>
|
||||||
<grid columns="2" id="thumb_box">
|
</vbox>
|
||||||
<label text="@.thumbnail_size" />
|
|
||||||
<slider min="1" max="10" id="zoom" cell_align="horizontal" width="128" />
|
|
||||||
|
|
||||||
<check id="thumb_overlay_enabled" text="@.overlay_size"/>
|
|
||||||
<slider min="2" max="10" id="thumb_overlay_size" cell_align="horizontal" width="128" />
|
|
||||||
<check id="thumb_scale_up_to_fit" text="@.scale_up_to_fit" cell_hspan="2" />
|
|
||||||
</grid>
|
|
||||||
</vbox>
|
|
||||||
</hbox>
|
|
||||||
|
|
||||||
<separator text="@.onion_skin" left="true" horizontal="true" />
|
<separator text="@.onion_skin" left="true" horizontal="true" />
|
||||||
<grid columns="2">
|
<grid columns="2">
|
||||||
|
|
|
@ -53,8 +53,6 @@ ConfigureTimelinePopup::ConfigureTimelinePopup()
|
||||||
m_box = new app::gen::TimelineConf();
|
m_box = new app::gen::TimelineConf();
|
||||||
addChild(m_box);
|
addChild(m_box);
|
||||||
|
|
||||||
m_box->position()->ItemChange.connect(
|
|
||||||
[this] { onChangeTimelinePosition(m_box->position()->selectedItem()); });
|
|
||||||
m_box->firstFrame()->Change.connect([this] { onChangeFirstFrame(); });
|
m_box->firstFrame()->Change.connect([this] { onChangeFirstFrame(); });
|
||||||
m_box->merge()->Click.connect([this] { onChangeType(); });
|
m_box->merge()->Click.connect([this] { onChangeType(); });
|
||||||
m_box->tint()->Click.connect([this] { onChangeType(); });
|
m_box->tint()->Click.connect([this] { onChangeType(); });
|
||||||
|
@ -94,15 +92,6 @@ void ConfigureTimelinePopup::updateWidgetsFromCurrentSettings()
|
||||||
DocumentPreferences& docPref = this->docPref();
|
DocumentPreferences& docPref = this->docPref();
|
||||||
base::ScopedValue lockUpdates(m_lockUpdates, true);
|
base::ScopedValue lockUpdates(m_lockUpdates, true);
|
||||||
|
|
||||||
auto position = Preferences::instance().general.timelinePosition();
|
|
||||||
int selItem = 2;
|
|
||||||
switch (position) {
|
|
||||||
case gen::TimelinePosition::LEFT: selItem = 0; break;
|
|
||||||
case gen::TimelinePosition::RIGHT: selItem = 1; break;
|
|
||||||
case gen::TimelinePosition::BOTTOM: selItem = 2; break;
|
|
||||||
}
|
|
||||||
m_box->position()->setSelectedItem(selItem, false);
|
|
||||||
|
|
||||||
m_box->firstFrame()->setTextf("%d", docPref.timeline.firstFrame());
|
m_box->firstFrame()->setTextf("%d", docPref.timeline.firstFrame());
|
||||||
|
|
||||||
switch (docPref.onionskin.type()) {
|
switch (docPref.onionskin.type()) {
|
||||||
|
@ -148,19 +137,6 @@ bool ConfigureTimelinePopup::onProcessMessage(ui::Message* msg)
|
||||||
return PopupWindow::onProcessMessage(msg);
|
return PopupWindow::onProcessMessage(msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ConfigureTimelinePopup::onChangeTimelinePosition(int option)
|
|
||||||
{
|
|
||||||
gen::TimelinePosition newTimelinePos = gen::TimelinePosition::BOTTOM;
|
|
||||||
|
|
||||||
int selItem = option;
|
|
||||||
switch (selItem) {
|
|
||||||
case 0: newTimelinePos = gen::TimelinePosition::LEFT; break;
|
|
||||||
case 1: newTimelinePos = gen::TimelinePosition::RIGHT; break;
|
|
||||||
case 2: newTimelinePos = gen::TimelinePosition::BOTTOM; break;
|
|
||||||
}
|
|
||||||
Preferences::instance().general.timelinePosition(newTimelinePos);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ConfigureTimelinePopup::onChangeFirstFrame()
|
void ConfigureTimelinePopup::onChangeFirstFrame()
|
||||||
{
|
{
|
||||||
docPref().timeline.firstFrame(m_box->firstFrame()->textInt());
|
docPref().timeline.firstFrame(m_box->firstFrame()->textInt());
|
||||||
|
|
|
@ -31,8 +31,6 @@ class ConfigureTimelinePopup : public ui::PopupWindow {
|
||||||
public:
|
public:
|
||||||
ConfigureTimelinePopup();
|
ConfigureTimelinePopup();
|
||||||
|
|
||||||
static void onChangeTimelinePosition(int option);
|
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
bool onProcessMessage(ui::Message* msg) override;
|
bool onProcessMessage(ui::Message* msg) override;
|
||||||
void onChangeFirstFrame();
|
void onChangeFirstFrame();
|
||||||
|
|
|
@ -10,9 +10,19 @@
|
||||||
|
|
||||||
#include "app/ui/dock.h"
|
#include "app/ui/dock.h"
|
||||||
|
|
||||||
|
#include "app/app.h"
|
||||||
|
#include "app/i18n/strings.h"
|
||||||
|
#include "app/ini_file.h"
|
||||||
|
#include "app/modules/gfx.h"
|
||||||
|
#include "app/pref/preferences.h"
|
||||||
#include "app/ui/dockable.h"
|
#include "app/ui/dockable.h"
|
||||||
|
#include "app/ui/layout_selector.h"
|
||||||
|
#include "app/ui/main_window.h"
|
||||||
#include "app/ui/skin/skin_theme.h"
|
#include "app/ui/skin/skin_theme.h"
|
||||||
|
#include "os/system.h"
|
||||||
#include "ui/cursor_type.h"
|
#include "ui/cursor_type.h"
|
||||||
|
#include "ui/label.h"
|
||||||
|
#include "ui/menu.h"
|
||||||
#include "ui/message.h"
|
#include "ui/message.h"
|
||||||
#include "ui/paint_event.h"
|
#include "ui/paint_event.h"
|
||||||
#include "ui/resize_event.h"
|
#include "ui/resize_event.h"
|
||||||
|
@ -54,34 +64,86 @@ int side_from_index(int index)
|
||||||
|
|
||||||
} // anonymous namespace
|
} // anonymous namespace
|
||||||
|
|
||||||
void DockTabs::onSizeHint(ui::SizeHintEvent& ev)
|
// TODO: Duplicated from main_window.cpp
|
||||||
|
static constexpr auto kLegacyLayoutMainWindowSection = "layout:main_window";
|
||||||
|
static constexpr auto kLegacyLayoutTimelineSplitter = "timeline_splitter";
|
||||||
|
|
||||||
|
Dock::DropzonePlaceholder::DropzonePlaceholder(Widget* dragWidget, const gfx::Point& mousePosition)
|
||||||
|
: Widget(kGenericWidget)
|
||||||
{
|
{
|
||||||
gfx::Size sz;
|
setExpansive(true);
|
||||||
for (auto child : children()) {
|
setSizeHint(dragWidget->sizeHint());
|
||||||
if (child->isVisible())
|
setMinSize(dragWidget->size());
|
||||||
sz |= child->sizeHint();
|
|
||||||
|
m_mouseOffset = mousePosition - dragWidget->bounds().origin();
|
||||||
|
|
||||||
|
const os::SurfaceRef surface = os::System::instance()->makeRgbaSurface(dragWidget->size().w,
|
||||||
|
dragWidget->size().h);
|
||||||
|
{
|
||||||
|
const os::SurfaceLock lock(surface.get());
|
||||||
|
Paint paint;
|
||||||
|
paint.color(gfx::rgba(0, 0, 0, 0));
|
||||||
|
paint.style(os::Paint::Fill);
|
||||||
|
surface->drawRect(gfx::Rect(0, 0, surface->width(), surface->height()), paint);
|
||||||
}
|
}
|
||||||
sz.h += textHeight();
|
|
||||||
ev.setSizeHint(sz);
|
{
|
||||||
|
Graphics g(display(), surface, 0, 0);
|
||||||
|
g.setFont(font());
|
||||||
|
|
||||||
|
Paint paint;
|
||||||
|
paint.color(gfx::rgba(0, 0, 0, 200));
|
||||||
|
|
||||||
|
// TODO: This will render any open things, especially the preview editor, need to close or hide
|
||||||
|
// that for a frame or paint the widget itself to a surface instead of croppping the backLayer.
|
||||||
|
auto backLayerSurface = display()->backLayer()->surface();
|
||||||
|
g.drawSurface(backLayerSurface.get(),
|
||||||
|
dragWidget->bounds(),
|
||||||
|
gfx::Rect(0, 0, surface->width(), surface->height()),
|
||||||
|
os::Sampling(),
|
||||||
|
&paint);
|
||||||
|
}
|
||||||
|
|
||||||
|
m_floatingUILayer = UILayer::Make();
|
||||||
|
m_floatingUILayer->setSurface(surface);
|
||||||
|
m_floatingUILayer->setPosition(dragWidget->bounds().origin());
|
||||||
|
display()->addLayer(m_floatingUILayer);
|
||||||
}
|
}
|
||||||
|
|
||||||
void DockTabs::onResize(ui::ResizeEvent& ev)
|
inline Dock::DropzonePlaceholder::~DropzonePlaceholder()
|
||||||
{
|
{
|
||||||
auto bounds = ev.bounds();
|
display()->removeLayer(m_floatingUILayer);
|
||||||
setBoundsQuietly(bounds);
|
|
||||||
bounds = childrenBounds();
|
|
||||||
bounds.y += textHeight();
|
|
||||||
bounds.h -= textHeight();
|
|
||||||
|
|
||||||
for (auto child : children()) {
|
|
||||||
child->setBounds(bounds);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void DockTabs::onPaint(ui::PaintEvent& ev)
|
void Dock::DropzonePlaceholder::setGhostPosition(const gfx::Point& position) const
|
||||||
|
{
|
||||||
|
ASSERT(m_floatingUILayer);
|
||||||
|
|
||||||
|
display()->dirtyRect(m_floatingUILayer->bounds());
|
||||||
|
m_floatingUILayer->setPosition(position - m_mouseOffset);
|
||||||
|
display()->dirtyRect(m_floatingUILayer->bounds());
|
||||||
|
}
|
||||||
|
|
||||||
|
void Dock::DropzonePlaceholder::onPaint(PaintEvent& ev)
|
||||||
{
|
{
|
||||||
Graphics* g = ev.graphics();
|
Graphics* g = ev.graphics();
|
||||||
g->fillRect(gfx::rgba(0, 0, 255), clientBounds());
|
gfx::Rect bounds = clientBounds();
|
||||||
|
|
||||||
|
g->fillRect(bgColor(), bounds);
|
||||||
|
|
||||||
|
bounds.shrink(2 * guiscale());
|
||||||
|
|
||||||
|
const auto* theme = SkinTheme::get(this);
|
||||||
|
const gfx::Color color = theme->colors.workspaceText();
|
||||||
|
|
||||||
|
g->drawRect(color, bounds);
|
||||||
|
g->drawLine(color, bounds.center(), bounds.origin());
|
||||||
|
g->drawLine(color, bounds.center(), bounds.point2());
|
||||||
|
g->drawLine(color, bounds.center(), bounds.point2() - gfx::Point(bounds.w, 0));
|
||||||
|
g->drawLine(color, bounds.center(), bounds.origin() + gfx::Point(bounds.w, 0));
|
||||||
|
g->drawRect(
|
||||||
|
color,
|
||||||
|
gfx::Rect(bounds.center() - gfx::Point(2, 2) * guiscale(), gfx::Size(4, 4) * guiscale()));
|
||||||
}
|
}
|
||||||
|
|
||||||
Dock::Dock()
|
Dock::Dock()
|
||||||
|
@ -104,10 +166,11 @@ void Dock::setCustomizing(bool enable, bool doLayout)
|
||||||
m_customizing = enable;
|
m_customizing = enable;
|
||||||
|
|
||||||
for (int i = 0; i < kSides; ++i) {
|
for (int i = 0; i < kSides; ++i) {
|
||||||
auto child = m_sides[i];
|
auto* child = m_sides[i];
|
||||||
if (!child)
|
if (!child)
|
||||||
continue;
|
continue;
|
||||||
else if (auto subdock = dynamic_cast<Dock*>(child))
|
|
||||||
|
if (auto* subdock = dynamic_cast<Dock*>(child))
|
||||||
subdock->setCustomizing(enable, false);
|
subdock->setCustomizing(enable, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -118,23 +181,16 @@ void Dock::setCustomizing(bool enable, bool doLayout)
|
||||||
void Dock::resetDocks()
|
void Dock::resetDocks()
|
||||||
{
|
{
|
||||||
for (int i = 0; i < kSides; ++i) {
|
for (int i = 0; i < kSides; ++i) {
|
||||||
auto child = m_sides[i];
|
auto* child = m_sides[i];
|
||||||
if (!child)
|
if (!child)
|
||||||
continue;
|
continue;
|
||||||
else if (auto subdock = dynamic_cast<Dock*>(child)) {
|
|
||||||
|
if (auto* subdock = dynamic_cast<Dock*>(child)) {
|
||||||
subdock->resetDocks();
|
subdock->resetDocks();
|
||||||
if (subdock->m_autoDelete)
|
if (subdock->m_autoDelete)
|
||||||
delete subdock;
|
delete subdock;
|
||||||
}
|
}
|
||||||
else if (auto tabs = dynamic_cast<DockTabs*>(child)) {
|
|
||||||
for (auto child2 : tabs->children()) {
|
|
||||||
if (auto subdock2 = dynamic_cast<Dock*>(child2)) {
|
|
||||||
subdock2->resetDocks();
|
|
||||||
if (subdock2->m_autoDelete)
|
|
||||||
delete subdock2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
m_sides[i] = nullptr;
|
m_sides[i] = nullptr;
|
||||||
}
|
}
|
||||||
removeAllChildren();
|
removeAllChildren();
|
||||||
|
@ -155,18 +211,8 @@ void Dock::dock(int side, ui::Widget* widget, const gfx::Size& prefSize)
|
||||||
else if (auto subdock = dynamic_cast<Dock*>(m_sides[i])) {
|
else if (auto subdock = dynamic_cast<Dock*>(m_sides[i])) {
|
||||||
subdock->dock(CENTER, widget, prefSize);
|
subdock->dock(CENTER, widget, prefSize);
|
||||||
}
|
}
|
||||||
else if (auto tabs = dynamic_cast<DockTabs*>(m_sides[i])) {
|
|
||||||
tabs->addChild(widget);
|
|
||||||
}
|
|
||||||
// If this side already contains a widget, we create a DockTabs in
|
|
||||||
// this side.
|
|
||||||
else {
|
else {
|
||||||
auto oldWidget = m_sides[i];
|
ASSERT(false); // Docking failure!
|
||||||
auto newTabs = new DockTabs;
|
|
||||||
replaceChild(oldWidget, newTabs);
|
|
||||||
newTabs->addChild(oldWidget);
|
|
||||||
newTabs->addChild(widget);
|
|
||||||
setSide(i, newTabs);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -180,7 +226,7 @@ void Dock::dockRelativeTo(ui::Widget* relative,
|
||||||
Widget* parent = relative->parent();
|
Widget* parent = relative->parent();
|
||||||
ASSERT(parent);
|
ASSERT(parent);
|
||||||
|
|
||||||
Dock* subdock = new Dock;
|
auto* subdock = new Dock;
|
||||||
subdock->m_autoDelete = true;
|
subdock->m_autoDelete = true;
|
||||||
subdock->m_customizing = m_customizing;
|
subdock->m_customizing = m_customizing;
|
||||||
parent->replaceChild(relative, subdock);
|
parent->replaceChild(relative, subdock);
|
||||||
|
@ -188,7 +234,7 @@ void Dock::dockRelativeTo(ui::Widget* relative,
|
||||||
subdock->dock(side, widget, prefSize);
|
subdock->dock(side, widget, prefSize);
|
||||||
|
|
||||||
// Fix the m_sides item if the parent is a Dock
|
// Fix the m_sides item if the parent is a Dock
|
||||||
if (auto relativeDock = dynamic_cast<Dock*>(parent)) {
|
if (auto* relativeDock = dynamic_cast<Dock*>(parent)) {
|
||||||
for (int i = 0; i < kSides; ++i) {
|
for (int i = 0; i < kSides; ++i) {
|
||||||
if (relativeDock->m_sides[i] == relative) {
|
if (relativeDock->m_sides[i] == relative) {
|
||||||
relativeDock->setSide(i, subdock);
|
relativeDock->setSide(i, subdock);
|
||||||
|
@ -204,12 +250,13 @@ void Dock::undock(Widget* widget)
|
||||||
if (!parent)
|
if (!parent)
|
||||||
return; // Already undocked
|
return; // Already undocked
|
||||||
|
|
||||||
if (auto parentDock = dynamic_cast<Dock*>(parent)) {
|
if (auto* parentDock = dynamic_cast<Dock*>(parent)) {
|
||||||
parentDock->removeChild(widget);
|
parentDock->removeChild(widget);
|
||||||
|
|
||||||
for (int i = 0; i < kSides; ++i) {
|
for (int i = 0; i < kSides; ++i) {
|
||||||
if (parentDock->m_sides[i] == widget) {
|
if (parentDock->m_sides[i] == widget) {
|
||||||
parentDock->setSide(i, nullptr);
|
parentDock->setSide(i, nullptr);
|
||||||
|
m_sizes[i] = gfx::Size();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -218,13 +265,6 @@ void Dock::undock(Widget* widget)
|
||||||
undock(parentDock);
|
undock(parentDock);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (auto parentTabs = dynamic_cast<DockTabs*>(parent)) {
|
|
||||||
parentTabs->removeChild(widget);
|
|
||||||
|
|
||||||
if (parentTabs->children().empty()) {
|
|
||||||
undock(parentTabs);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
else {
|
||||||
parent->removeChild(widget);
|
parent->removeChild(widget);
|
||||||
}
|
}
|
||||||
|
@ -238,25 +278,25 @@ int Dock::whichSideChildIsDocked(const ui::Widget* widget) const
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
gfx::Size Dock::getUserDefinedSizeAtSide(int side) const
|
const gfx::Size Dock::getUserDefinedSizeAtSide(int side) const
|
||||||
{
|
{
|
||||||
int i = side_index(side);
|
int i = side_index(side);
|
||||||
// Only EXPANSIVE sides can be user-defined (has a splitter so the
|
// Only EXPANSIVE sides can be user-defined (has a splitter so the
|
||||||
// user can expand or shrink it)
|
// user can expand or shrink it)
|
||||||
if (m_aligns[i] & EXPANSIVE)
|
if (m_aligns[i] & EXPANSIVE)
|
||||||
return m_sizes[i];
|
return m_sizes[i];
|
||||||
else
|
|
||||||
return gfx::Size();
|
return gfx::Size();
|
||||||
}
|
}
|
||||||
|
|
||||||
Dock* Dock::subdock(int side)
|
Dock* Dock::subdock(int side)
|
||||||
{
|
{
|
||||||
int i = side_index(side);
|
int i = side_index(side);
|
||||||
if (auto subdock = dynamic_cast<Dock*>(m_sides[i]))
|
if (auto* subdock = dynamic_cast<Dock*>(m_sides[i]))
|
||||||
return subdock;
|
return subdock;
|
||||||
|
|
||||||
auto oldWidget = m_sides[i];
|
auto* oldWidget = m_sides[i];
|
||||||
auto newSubdock = new Dock;
|
auto* newSubdock = new Dock;
|
||||||
newSubdock->m_autoDelete = true;
|
newSubdock->m_autoDelete = true;
|
||||||
newSubdock->m_customizing = m_customizing;
|
newSubdock->m_customizing = m_customizing;
|
||||||
setSide(i, newSubdock);
|
setSide(i, newSubdock);
|
||||||
|
@ -291,7 +331,7 @@ void Dock::onSizeHint(ui::SizeHintEvent& ev)
|
||||||
|
|
||||||
void Dock::onResize(ui::ResizeEvent& ev)
|
void Dock::onResize(ui::ResizeEvent& ev)
|
||||||
{
|
{
|
||||||
auto bounds = ev.bounds();
|
gfx::Rect bounds = ev.bounds();
|
||||||
setBoundsQuietly(bounds);
|
setBoundsQuietly(bounds);
|
||||||
bounds = childrenBounds();
|
bounds = childrenBounds();
|
||||||
|
|
||||||
|
@ -302,11 +342,11 @@ void Dock::onResize(ui::ResizeEvent& ev)
|
||||||
const gfx::Rect& widgetBounds,
|
const gfx::Rect& widgetBounds,
|
||||||
const gfx::Rect& separator,
|
const gfx::Rect& separator,
|
||||||
const int index) {
|
const int index) {
|
||||||
auto rc = widgetBounds;
|
gfx::Rect rc = widgetBounds;
|
||||||
auto th = textHeight();
|
auto th = textHeight();
|
||||||
if (isCustomizing()) {
|
if (isCustomizing()) {
|
||||||
int handleSide = 0;
|
int handleSide = 0;
|
||||||
if (auto dockable = dynamic_cast<Dockable*>(widget))
|
if (auto* dockable = dynamic_cast<Dockable*>(widget))
|
||||||
handleSide = dockable->dockHandleSide();
|
handleSide = dockable->dockHandleSide();
|
||||||
switch (handleSide) {
|
switch (handleSide) {
|
||||||
case ui::TOP:
|
case ui::TOP:
|
||||||
|
@ -326,7 +366,8 @@ void Dock::onResize(ui::ResizeEvent& ev)
|
||||||
void Dock::onPaint(ui::PaintEvent& ev)
|
void Dock::onPaint(ui::PaintEvent& ev)
|
||||||
{
|
{
|
||||||
Graphics* g = ev.graphics();
|
Graphics* g = ev.graphics();
|
||||||
gfx::Rect bounds = clientBounds();
|
|
||||||
|
const gfx::Rect& bounds = clientBounds();
|
||||||
g->fillRect(bgColor(), bounds);
|
g->fillRect(bgColor(), bounds);
|
||||||
|
|
||||||
if (isCustomizing()) {
|
if (isCustomizing()) {
|
||||||
|
@ -335,13 +376,13 @@ void Dock::onPaint(ui::PaintEvent& ev)
|
||||||
const gfx::Rect& widgetBounds,
|
const gfx::Rect& widgetBounds,
|
||||||
const gfx::Rect& separator,
|
const gfx::Rect& separator,
|
||||||
const int index) {
|
const int index) {
|
||||||
auto rc = widgetBounds;
|
gfx::Rect rc = widgetBounds;
|
||||||
auto th = textHeight();
|
auto th = textHeight();
|
||||||
if (isCustomizing()) {
|
if (isCustomizing()) {
|
||||||
auto theme = SkinTheme::get(this);
|
auto* theme = SkinTheme::get(this);
|
||||||
auto color = theme->colors.workspaceText();
|
const auto& color = theme->colors.workspaceText();
|
||||||
int handleSide = 0;
|
int handleSide = 0;
|
||||||
if (auto dockable = dynamic_cast<Dockable*>(widget))
|
if (auto* dockable = dynamic_cast<Dockable*>(widget))
|
||||||
handleSide = dockable->dockHandleSide();
|
handleSide = dockable->dockHandleSide();
|
||||||
switch (handleSide) {
|
switch (handleSide) {
|
||||||
case ui::TOP:
|
case ui::TOP:
|
||||||
|
@ -377,19 +418,20 @@ bool Dock::onProcessMessage(ui::Message* msg)
|
||||||
{
|
{
|
||||||
switch (msg->type()) {
|
switch (msg->type()) {
|
||||||
case kMouseDownMessage: {
|
case kMouseDownMessage: {
|
||||||
const gfx::Point pos = static_cast<MouseMessage*>(msg)->position();
|
auto* mouseMessage = static_cast<MouseMessage*>(msg);
|
||||||
|
const gfx::Point& pos = mouseMessage->position();
|
||||||
|
|
||||||
if (m_hit.sideIndex >= 0 || m_hit.dockable) {
|
if (m_hit.sideIndex >= 0 || m_hit.dockable) {
|
||||||
m_startPos = pos;
|
m_startPos = pos;
|
||||||
|
|
||||||
if (m_hit.sideIndex >= 0) {
|
if (m_hit.sideIndex >= 0)
|
||||||
m_startSize = m_sizes[m_hit.sideIndex];
|
m_startSize = m_sizes[m_hit.sideIndex];
|
||||||
}
|
|
||||||
|
|
||||||
captureMouse();
|
captureMouse();
|
||||||
|
|
||||||
if (m_hit.dockable)
|
if (m_hit.dockable && !mouseMessage->right()) {
|
||||||
invalidate();
|
m_dragging = true;
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -398,22 +440,98 @@ bool Dock::onProcessMessage(ui::Message* msg)
|
||||||
|
|
||||||
case kMouseMoveMessage: {
|
case kMouseMoveMessage: {
|
||||||
if (hasCapture()) {
|
if (hasCapture()) {
|
||||||
|
const gfx::Point& pos = static_cast<MouseMessage*>(msg)->position();
|
||||||
|
|
||||||
|
if (m_dropzonePlaceholder)
|
||||||
|
m_dropzonePlaceholder->setGhostPosition(pos);
|
||||||
|
|
||||||
if (m_hit.sideIndex >= 0) {
|
if (m_hit.sideIndex >= 0) {
|
||||||
const gfx::Point pos = static_cast<MouseMessage*>(msg)->position();
|
if (!display()->bounds().contains(pos) ||
|
||||||
|
(m_hit.widget && m_hit.widget->parent() &&
|
||||||
|
!m_hit.widget->parent()->bounds().contains(pos)))
|
||||||
|
break; // Do not handle anything outside bounds.
|
||||||
|
|
||||||
gfx::Size& sz = m_sizes[m_hit.sideIndex];
|
gfx::Size& sz = m_sizes[m_hit.sideIndex];
|
||||||
|
gfx::Size minSize(16 * guiscale(), 16 * guiscale());
|
||||||
|
|
||||||
|
if (m_hit.widget) {
|
||||||
|
minSize.w = std::max(m_hit.widget->minSize().w, minSize.w);
|
||||||
|
minSize.h = std::max(m_hit.widget->minSize().h, minSize.h);
|
||||||
|
}
|
||||||
|
|
||||||
switch (m_hit.sideIndex) {
|
switch (m_hit.sideIndex) {
|
||||||
case kTopIndex: sz.h = (m_startSize.h + pos.y - m_startPos.y); break;
|
case kTopIndex: sz.h = std::max(m_startSize.h + pos.y - m_startPos.y, minSize.h); break;
|
||||||
case kBottomIndex: sz.h = (m_startSize.h - pos.y + m_startPos.y); break;
|
case kBottomIndex:
|
||||||
case kLeftIndex: sz.w = (m_startSize.w + pos.x - m_startPos.x); break;
|
sz.h = std::max(m_startSize.h - pos.y + m_startPos.y, minSize.h);
|
||||||
case kRightIndex: sz.w = (m_startSize.w - pos.x + m_startPos.x); break;
|
break;
|
||||||
|
case kLeftIndex:
|
||||||
|
sz.w = std::max(m_startSize.w + pos.x - m_startPos.x, minSize.w);
|
||||||
|
break;
|
||||||
|
case kRightIndex:
|
||||||
|
sz.w = std::max(m_startSize.w - pos.x + m_startPos.x, minSize.w);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
layout();
|
layout();
|
||||||
Resize();
|
Resize();
|
||||||
}
|
}
|
||||||
else if (m_hit.dockable) {
|
else if (m_hit.dockable && m_dragging) {
|
||||||
invalidate();
|
invalidate();
|
||||||
|
|
||||||
|
auto* parentDock = dynamic_cast<Dock*>(m_hit.widget->parent());
|
||||||
|
ASSERT(parentDock);
|
||||||
|
|
||||||
|
if (!parentDock)
|
||||||
|
break;
|
||||||
|
|
||||||
|
if (!m_dropzonePlaceholder)
|
||||||
|
m_dropzonePlaceholder.reset(new DropzonePlaceholder(m_hit.widget, pos));
|
||||||
|
|
||||||
|
auto dockedAt = parentDock->whichSideChildIsDocked(m_hit.widget);
|
||||||
|
const auto& bounds = parentDock->bounds();
|
||||||
|
|
||||||
|
if (!bounds.contains(pos))
|
||||||
|
break; // Do not handle anything outside the bounds of the dock.
|
||||||
|
|
||||||
|
const int kBufferZone =
|
||||||
|
std::max(12 * guiscale(), std::min(m_hit.widget->size().w, m_hit.widget->size().h));
|
||||||
|
|
||||||
|
int newTargetSide = -1;
|
||||||
|
if (m_hit.dockable->dockableAt() & LEFT && !(dockedAt & LEFT) &&
|
||||||
|
pos.x < bounds.x + kBufferZone) {
|
||||||
|
newTargetSide = LEFT;
|
||||||
|
}
|
||||||
|
else if (m_hit.dockable->dockableAt() & RIGHT && !(dockedAt & RIGHT) &&
|
||||||
|
pos.x > (bounds.w - kBufferZone)) {
|
||||||
|
newTargetSide = RIGHT;
|
||||||
|
}
|
||||||
|
else if (m_hit.dockable->dockableAt() & TOP && !(dockedAt & TOP) &&
|
||||||
|
pos.y < bounds.y + kBufferZone) {
|
||||||
|
newTargetSide = TOP;
|
||||||
|
}
|
||||||
|
else if (m_hit.dockable->dockableAt() & BOTTOM && !(dockedAt & BOTTOM) &&
|
||||||
|
pos.y > (bounds.h - kBufferZone)) {
|
||||||
|
newTargetSide = BOTTOM;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_hit.targetSide == newTargetSide)
|
||||||
|
break;
|
||||||
|
|
||||||
|
m_hit.targetSide = newTargetSide;
|
||||||
|
|
||||||
|
// Always undock the placeholder
|
||||||
|
if (m_dropzonePlaceholder && m_dropzonePlaceholder->parent()) {
|
||||||
|
auto* placeholderCurrentDock = dynamic_cast<Dock*>(m_dropzonePlaceholder->parent());
|
||||||
|
placeholderCurrentDock->undock(m_dropzonePlaceholder.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_dropzonePlaceholder && m_hit.targetSide != -1) {
|
||||||
|
parentDock->dock(m_hit.targetSide,
|
||||||
|
m_dropzonePlaceholder.get(),
|
||||||
|
m_hit.widget->sizeHint());
|
||||||
|
}
|
||||||
|
|
||||||
|
layout();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
@ -422,13 +540,76 @@ bool Dock::onProcessMessage(ui::Message* msg)
|
||||||
case kMouseUpMessage: {
|
case kMouseUpMessage: {
|
||||||
if (hasCapture()) {
|
if (hasCapture()) {
|
||||||
releaseMouse();
|
releaseMouse();
|
||||||
onUserResizedDock();
|
const auto* mouseMessage = static_cast<MouseMessage*>(msg);
|
||||||
|
|
||||||
|
if (m_dropzonePlaceholder && m_dropzonePlaceholder->parent()) {
|
||||||
|
// Always undock the dropzone placeholder to avoid dangling sizes.
|
||||||
|
auto* placeholderCurrentDock = dynamic_cast<Dock*>(m_dropzonePlaceholder->parent());
|
||||||
|
placeholderCurrentDock->undock(m_dropzonePlaceholder.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_hit.dockable) {
|
||||||
|
auto* dockableWidget = dynamic_cast<Widget*>(m_hit.dockable);
|
||||||
|
auto* widgetDock = dynamic_cast<Dock*>(dockableWidget->parent());
|
||||||
|
|
||||||
|
int currentSide = widgetDock->whichSideChildIsDocked(dockableWidget);
|
||||||
|
|
||||||
|
assert(dockableWidget && widgetDock);
|
||||||
|
|
||||||
|
if (mouseMessage->right() && !m_dragging) {
|
||||||
|
Menu menu;
|
||||||
|
MenuItem left(Strings::dock_left());
|
||||||
|
MenuItem right(Strings::dock_right());
|
||||||
|
MenuItem top(Strings::dock_top());
|
||||||
|
MenuItem bottom(Strings::dock_bottom());
|
||||||
|
|
||||||
|
if (m_hit.dockable->dockableAt() & ui::LEFT) {
|
||||||
|
menu.addChild(&left);
|
||||||
|
}
|
||||||
|
if (m_hit.dockable->dockableAt() & ui::RIGHT) {
|
||||||
|
menu.addChild(&right);
|
||||||
|
}
|
||||||
|
if (m_hit.dockable->dockableAt() & ui::TOP) {
|
||||||
|
menu.addChild(&top);
|
||||||
|
}
|
||||||
|
if (m_hit.dockable->dockableAt() & ui::BOTTOM) {
|
||||||
|
menu.addChild(&bottom);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (currentSide) {
|
||||||
|
case ui::LEFT: left.setEnabled(false); break;
|
||||||
|
case ui::RIGHT: right.setEnabled(false); break;
|
||||||
|
case ui::TOP: top.setEnabled(false); break;
|
||||||
|
case ui::BOTTOM: bottom.setEnabled(false); break;
|
||||||
|
}
|
||||||
|
|
||||||
|
left.Click.connect([&] { redockWidget(widgetDock, dockableWidget, ui::LEFT); });
|
||||||
|
right.Click.connect([&] { redockWidget(widgetDock, dockableWidget, ui::RIGHT); });
|
||||||
|
top.Click.connect([&] { redockWidget(widgetDock, dockableWidget, ui::TOP); });
|
||||||
|
bottom.Click.connect([&] { redockWidget(widgetDock, dockableWidget, ui::BOTTOM); });
|
||||||
|
|
||||||
|
menu.showPopup(mouseMessage->position(), display());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else if (m_hit.targetSide > 0 && m_dragging) {
|
||||||
|
ASSERT(m_hit.dockable->dockableAt() & m_hit.targetSide);
|
||||||
|
redockWidget(widgetDock, dockableWidget, m_hit.targetSide);
|
||||||
|
m_dropzonePlaceholder = nullptr;
|
||||||
|
m_dragging = false;
|
||||||
|
m_hit = Hit();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m_dropzonePlaceholder = nullptr;
|
||||||
|
m_dragging = false;
|
||||||
|
m_hit = Hit();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case kSetCursorMessage: {
|
case kSetCursorMessage: {
|
||||||
const gfx::Point pos = static_cast<MouseMessage*>(msg)->position();
|
const gfx::Point& pos = static_cast<MouseMessage*>(msg)->position();
|
||||||
ui::CursorType cursor = ui::kArrowCursor;
|
ui::CursorType cursor = ui::kArrowCursor;
|
||||||
|
|
||||||
if (!hasCapture())
|
if (!hasCapture())
|
||||||
|
@ -442,59 +623,10 @@ bool Dock::onProcessMessage(ui::Message* msg)
|
||||||
case kRightIndex: cursor = ui::kSizeWECursor; break;
|
case kRightIndex: cursor = ui::kSizeWECursor; break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (m_hit.dockable) {
|
else if (m_hit.dockable && m_hit.targetSide == -1) {
|
||||||
cursor = ui::kMoveCursor;
|
cursor = ui::kMoveCursor;
|
||||||
}
|
}
|
||||||
|
|
||||||
#if 0
|
|
||||||
m_hit = Hit();
|
|
||||||
forEachSide(
|
|
||||||
childrenBounds(),
|
|
||||||
[this, pos, &cursor](ui::Widget* widget,
|
|
||||||
const gfx::Rect& widgetBounds,
|
|
||||||
const gfx::Rect& separator,
|
|
||||||
const int index) {
|
|
||||||
if (separator.contains(pos)) {
|
|
||||||
m_hit.widget = widget;
|
|
||||||
m_hit.sideIndex = index;
|
|
||||||
|
|
||||||
if (index == kTopIndex || index == kBottomIndex) {
|
|
||||||
cursor = ui::kSizeNSCursor;
|
|
||||||
}
|
|
||||||
else if (index == kLeftIndex || index == kRightIndex) {
|
|
||||||
cursor = ui::kSizeWECursor;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (isCustomizing()) {
|
|
||||||
auto th = textHeight();
|
|
||||||
auto rc = widgetBounds;
|
|
||||||
auto theme = SkinTheme::get(this);
|
|
||||||
auto color = theme->colors.workspaceText();
|
|
||||||
if (auto dockable = dynamic_cast<Dockable*>(widget)) {
|
|
||||||
int handleSide = dockable->dockHandleSide();
|
|
||||||
switch (handleSide) {
|
|
||||||
case ui::TOP:
|
|
||||||
rc.h = th;
|
|
||||||
if (rc.contains(pos)) {
|
|
||||||
cursor = ui::kMoveCursor;
|
|
||||||
m_hit.widget = widget;
|
|
||||||
m_hit.dockable = dockable;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case ui::LEFT:
|
|
||||||
rc.w = th;
|
|
||||||
if (rc.contains(pos)) {
|
|
||||||
cursor = ui::kMoveCursor;
|
|
||||||
m_hit.widget = widget;
|
|
||||||
m_hit.dockable = dockable;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
#endif
|
|
||||||
|
|
||||||
ui::set_mouse_cursor(cursor);
|
ui::set_mouse_cursor(cursor);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -511,7 +643,7 @@ void Dock::onUserResizedDock()
|
||||||
|
|
||||||
// Send the same notification for the parent (as probably eh
|
// Send the same notification for the parent (as probably eh
|
||||||
// MainWindow is listening the signal of just the root dock).
|
// MainWindow is listening the signal of just the root dock).
|
||||||
if (auto parentDock = dynamic_cast<Dock*>(parent())) {
|
if (auto* parentDock = dynamic_cast<Dock*>(parent())) {
|
||||||
parentDock->onUserResizedDock();
|
parentDock->onUserResizedDock();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -533,19 +665,10 @@ int Dock::calcAlign(const int i)
|
||||||
if (!widget) {
|
if (!widget) {
|
||||||
// Do nothing
|
// Do nothing
|
||||||
}
|
}
|
||||||
else if (auto subdock = dynamic_cast<Dock*>(widget)) {
|
else if (auto* subdock = dynamic_cast<Dock*>(widget)) {
|
||||||
align = subdock->calcAlign(i);
|
align = subdock->calcAlign(i);
|
||||||
}
|
}
|
||||||
else if (auto tabs = dynamic_cast<DockTabs*>(widget)) {
|
else if (auto* dockable2 = dynamic_cast<Dockable*>(widget)) {
|
||||||
for (auto child : tabs->children()) {
|
|
||||||
if (auto subdock2 = dynamic_cast<Dock*>(widget))
|
|
||||||
align |= subdock2->calcAlign(i);
|
|
||||||
else if (auto dockable = dynamic_cast<Dockable*>(child)) {
|
|
||||||
align = dockable->dockableAt();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (auto dockable2 = dynamic_cast<Dockable*>(widget)) {
|
|
||||||
align = dockable2->dockableAt();
|
align = dockable2->dockableAt();
|
||||||
}
|
}
|
||||||
return align;
|
return align;
|
||||||
|
@ -560,28 +683,15 @@ void Dock::updateDockVisibility()
|
||||||
if (!widget)
|
if (!widget)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
if (auto subdock = dynamic_cast<Dock*>(widget)) {
|
if (auto* subdock = dynamic_cast<Dock*>(widget)) {
|
||||||
subdock->updateDockVisibility();
|
subdock->updateDockVisibility();
|
||||||
}
|
}
|
||||||
else if (auto tabs = dynamic_cast<DockTabs*>(widget)) {
|
|
||||||
bool visible2 = false;
|
|
||||||
for (auto child : tabs->children()) {
|
|
||||||
if (auto subdock2 = dynamic_cast<Dock*>(widget)) {
|
|
||||||
subdock2->updateDockVisibility();
|
|
||||||
}
|
|
||||||
if (child->isVisible()) {
|
|
||||||
visible2 = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tabs->setVisible(visible2);
|
|
||||||
if (visible2)
|
|
||||||
visible = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (widget->isVisible()) {
|
if (widget->isVisible()) {
|
||||||
visible = true;
|
visible = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setVisible(visible);
|
setVisible(visible);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -592,8 +702,8 @@ void Dock::forEachSide(gfx::Rect bounds,
|
||||||
const int index)> f)
|
const int index)> f)
|
||||||
{
|
{
|
||||||
for (int i = 0; i < kSides; ++i) {
|
for (int i = 0; i < kSides; ++i) {
|
||||||
auto widget = m_sides[i];
|
auto* widget = m_sides[i];
|
||||||
if (!widget || !widget->isVisible()) {
|
if (!widget || !widget->isVisible() || widget->isDecorative()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -650,6 +760,38 @@ void Dock::forEachSide(gfx::Rect bounds,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Dock::redockWidget(app::Dock* widgetDock, ui::Widget* dockableWidget, const int side)
|
||||||
|
{
|
||||||
|
const gfx::Rect workspaceBounds = widgetDock->bounds();
|
||||||
|
|
||||||
|
gfx::Size size;
|
||||||
|
if (dockableWidget->id() == "timeline") {
|
||||||
|
size.w = 64;
|
||||||
|
size.h = 64;
|
||||||
|
auto timelineSplitterPos =
|
||||||
|
get_config_double(kLegacyLayoutMainWindowSection, kLegacyLayoutTimelineSplitter, 75.0) /
|
||||||
|
100.0;
|
||||||
|
auto pos = gen::TimelinePosition::LEFT;
|
||||||
|
size.w = (workspaceBounds.w * (1.0 - timelineSplitterPos)) / guiscale();
|
||||||
|
|
||||||
|
if (side & RIGHT) {
|
||||||
|
pos = gen::TimelinePosition::RIGHT;
|
||||||
|
}
|
||||||
|
if (side & BOTTOM || side & TOP) {
|
||||||
|
pos = gen::TimelinePosition::BOTTOM;
|
||||||
|
size.h = (workspaceBounds.h * (1.0 - timelineSplitterPos)) / guiscale();
|
||||||
|
}
|
||||||
|
Preferences::instance().general.timelinePosition(pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
widgetDock->undock(dockableWidget);
|
||||||
|
widgetDock->dock(side, dockableWidget, size);
|
||||||
|
|
||||||
|
App::instance()->mainWindow()->invalidate();
|
||||||
|
layout();
|
||||||
|
onUserResizedDock();
|
||||||
|
}
|
||||||
|
|
||||||
Dock::Hit Dock::calcHit(const gfx::Point& pos)
|
Dock::Hit Dock::calcHit(const gfx::Point& pos)
|
||||||
{
|
{
|
||||||
Hit hit;
|
Hit hit;
|
||||||
|
@ -664,11 +806,9 @@ Dock::Hit Dock::calcHit(const gfx::Point& pos)
|
||||||
}
|
}
|
||||||
else if (isCustomizing()) {
|
else if (isCustomizing()) {
|
||||||
auto th = textHeight();
|
auto th = textHeight();
|
||||||
auto rc = widgetBounds;
|
gfx::Rect rc = widgetBounds;
|
||||||
auto theme = SkinTheme::get(this);
|
if (auto* dockable = dynamic_cast<Dockable*>(widget)) {
|
||||||
if (auto dockable = dynamic_cast<Dockable*>(widget)) {
|
switch (dockable->dockHandleSide()) {
|
||||||
int handleSide = dockable->dockHandleSide();
|
|
||||||
switch (handleSide) {
|
|
||||||
case ui::TOP:
|
case ui::TOP:
|
||||||
rc.h = th;
|
rc.h = th;
|
||||||
if (rc.contains(pos)) {
|
if (rc.contains(pos)) {
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
#define APP_UI_DOCK_H_INCLUDED
|
#define APP_UI_DOCK_H_INCLUDED
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include "app/ui/dockable.h"
|
||||||
#include "gfx/rect.h"
|
#include "gfx/rect.h"
|
||||||
#include "gfx/size.h"
|
#include "gfx/size.h"
|
||||||
#include "ui/widget.h"
|
#include "ui/widget.h"
|
||||||
|
@ -21,18 +22,26 @@ namespace app {
|
||||||
|
|
||||||
class Dockable;
|
class Dockable;
|
||||||
|
|
||||||
class DockTabs : public ui::Widget {
|
|
||||||
public:
|
|
||||||
protected:
|
|
||||||
void onSizeHint(ui::SizeHintEvent& ev) override;
|
|
||||||
void onResize(ui::ResizeEvent& ev) override;
|
|
||||||
void onPaint(ui::PaintEvent& ev) override;
|
|
||||||
};
|
|
||||||
|
|
||||||
class Dock : public ui::Widget {
|
class Dock : public ui::Widget {
|
||||||
public:
|
public:
|
||||||
static constexpr const int kSides = 5;
|
static constexpr const int kSides = 5;
|
||||||
|
|
||||||
|
class DropzonePlaceholder final : public Widget,
|
||||||
|
public Dockable {
|
||||||
|
public:
|
||||||
|
explicit DropzonePlaceholder(Widget* dragWidget, const gfx::Point& mousePosition);
|
||||||
|
~DropzonePlaceholder() override;
|
||||||
|
void setGhostPosition(const gfx::Point& position) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void onPaint(ui::PaintEvent& ev) override;
|
||||||
|
int dockHandleSide() const override { return 0; }
|
||||||
|
|
||||||
|
gfx::Point m_mouseOffset;
|
||||||
|
ui::UILayerRef m_floatingUILayer;
|
||||||
|
bool m_hidePreview;
|
||||||
|
};
|
||||||
|
|
||||||
Dock();
|
Dock();
|
||||||
|
|
||||||
bool isCustomizing() const { return m_customizing; }
|
bool isCustomizing() const { return m_customizing; }
|
||||||
|
@ -60,7 +69,7 @@ public:
|
||||||
|
|
||||||
// Functions useful to query/save the dock layout.
|
// Functions useful to query/save the dock layout.
|
||||||
int whichSideChildIsDocked(const ui::Widget* widget) const;
|
int whichSideChildIsDocked(const ui::Widget* widget) const;
|
||||||
gfx::Size getUserDefinedSizeAtSide(int side) const;
|
const gfx::Size getUserDefinedSizeAtSide(int side) const;
|
||||||
|
|
||||||
obs::signal<void()> Resize;
|
obs::signal<void()> Resize;
|
||||||
obs::signal<void()> UserResizedDock;
|
obs::signal<void()> UserResizedDock;
|
||||||
|
@ -74,8 +83,8 @@ protected:
|
||||||
void onUserResizedDock();
|
void onUserResizedDock();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void setSide(const int i, ui::Widget* newWidget);
|
void setSide(int i, ui::Widget* newWidget);
|
||||||
int calcAlign(const int i);
|
int calcAlign(int i);
|
||||||
void updateDockVisibility();
|
void updateDockVisibility();
|
||||||
void forEachSide(gfx::Rect bounds,
|
void forEachSide(gfx::Rect bounds,
|
||||||
std::function<void(ui::Widget* widget,
|
std::function<void(ui::Widget* widget,
|
||||||
|
@ -84,11 +93,13 @@ private:
|
||||||
const int index)> f);
|
const int index)> f);
|
||||||
|
|
||||||
bool hasVisibleSide(const int i) const { return (m_sides[i] && m_sides[i]->isVisible()); }
|
bool hasVisibleSide(const int i) const { return (m_sides[i] && m_sides[i]->isVisible()); }
|
||||||
|
void redockWidget(app::Dock* widgetDock, ui::Widget* dockableWidget, const int side);
|
||||||
|
|
||||||
struct Hit {
|
struct Hit {
|
||||||
ui::Widget* widget = nullptr;
|
ui::Widget* widget = nullptr;
|
||||||
Dockable* dockable = nullptr;
|
Dockable* dockable = nullptr;
|
||||||
int sideIndex = -1;
|
int sideIndex = -1;
|
||||||
|
int targetSide = -1;
|
||||||
};
|
};
|
||||||
|
|
||||||
Hit calcHit(const gfx::Point& pos);
|
Hit calcHit(const gfx::Point& pos);
|
||||||
|
@ -107,6 +118,10 @@ private:
|
||||||
|
|
||||||
// True when we paint/can drag-and-drop dockable widgets from handles.
|
// True when we paint/can drag-and-drop dockable widgets from handles.
|
||||||
bool m_customizing = false;
|
bool m_customizing = false;
|
||||||
|
|
||||||
|
// True when we're dragging a widget to attempt to dock it somewhere else.
|
||||||
|
bool m_dragging = false;
|
||||||
|
std::unique_ptr<DropzonePlaceholder> m_dropzonePlaceholder;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace app
|
} // namespace app
|
||||||
|
|
|
@ -36,10 +36,10 @@ void IconButton::setIcon(const skin::SkinPartPtr& part)
|
||||||
|
|
||||||
void IconButton::onInitTheme(InitThemeEvent& ev)
|
void IconButton::onInitTheme(InitThemeEvent& ev)
|
||||||
{
|
{
|
||||||
Button::onInitTheme(ev);
|
|
||||||
|
|
||||||
auto theme = SkinTheme::get(this);
|
auto theme = SkinTheme::get(this);
|
||||||
setBgColor(theme->colors.menuitemNormalFace());
|
setBgColor(theme->colors.menuitemNormalFace());
|
||||||
|
|
||||||
|
Button::onInitTheme(ev);
|
||||||
}
|
}
|
||||||
|
|
||||||
void IconButton::onSizeHint(SizeHintEvent& ev)
|
void IconButton::onSizeHint(SizeHintEvent& ev)
|
||||||
|
|
|
@ -33,9 +33,9 @@ using namespace tinyxml2;
|
||||||
|
|
||||||
static void save_dock_layout(XMLElement* elem, const Dock* dock)
|
static void save_dock_layout(XMLElement* elem, const Dock* dock)
|
||||||
{
|
{
|
||||||
for (const auto child : dock->children()) {
|
for (const auto* child : dock->children()) {
|
||||||
const int side = dock->whichSideChildIsDocked(child);
|
const int side = dock->whichSideChildIsDocked(child);
|
||||||
const gfx::Size size = dock->getUserDefinedSizeAtSide(side);
|
const gfx::Size& size = dock->getUserDefinedSizeAtSide(side);
|
||||||
|
|
||||||
std::string sideStr;
|
std::string sideStr;
|
||||||
switch (side) {
|
switch (side) {
|
||||||
|
@ -44,13 +44,14 @@ static void save_dock_layout(XMLElement* elem, const Dock* dock)
|
||||||
case ui::TOP: sideStr = "top"; break;
|
case ui::TOP: sideStr = "top"; break;
|
||||||
case ui::BOTTOM: sideStr = "bottom"; break;
|
case ui::BOTTOM: sideStr = "bottom"; break;
|
||||||
case ui::CENTER:
|
case ui::CENTER:
|
||||||
|
default:
|
||||||
// Empty side attribute
|
// Empty side attribute
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
XMLElement* childElem = elem->InsertNewChildElement("");
|
XMLElement* childElem = elem->InsertNewChildElement("");
|
||||||
|
|
||||||
if (auto subdock = dynamic_cast<const Dock*>(child)) {
|
if (const auto* subdock = dynamic_cast<const Dock*>(child)) {
|
||||||
childElem->SetValue("dock");
|
childElem->SetValue("dock");
|
||||||
if (!sideStr.empty())
|
if (!sideStr.empty())
|
||||||
childElem->SetAttribute("side", sideStr.c_str());
|
childElem->SetAttribute("side", sideStr.c_str());
|
||||||
|
@ -87,7 +88,7 @@ static void load_dock_layout(const XMLElement* elem, Dock* dock)
|
||||||
Dock* subdock = nullptr;
|
Dock* subdock = nullptr;
|
||||||
|
|
||||||
int side = ui::CENTER;
|
int side = ui::CENTER;
|
||||||
if (auto sideStr = elem->Attribute("side")) {
|
if (const auto* sideStr = elem->Attribute("side")) {
|
||||||
if (std::strcmp(sideStr, "left") == 0)
|
if (std::strcmp(sideStr, "left") == 0)
|
||||||
side = ui::LEFT;
|
side = ui::LEFT;
|
||||||
if (std::strcmp(sideStr, "right") == 0)
|
if (std::strcmp(sideStr, "right") == 0)
|
||||||
|
@ -129,7 +130,7 @@ static void load_dock_layout(const XMLElement* elem, Dock* dock)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (subdock) {
|
if (subdock) {
|
||||||
auto childElem = elem->FirstChildElement();
|
const auto* childElem = elem->FirstChildElement();
|
||||||
while (childElem) {
|
while (childElem) {
|
||||||
load_dock_layout(childElem, subdock);
|
load_dock_layout(childElem, subdock);
|
||||||
childElem = childElem->NextSiblingElement();
|
childElem = childElem->NextSiblingElement();
|
||||||
|
@ -143,12 +144,27 @@ static void load_dock_layout(const XMLElement* elem, Dock* dock)
|
||||||
// static
|
// static
|
||||||
LayoutPtr Layout::MakeFromXmlElement(const XMLElement* layoutElem)
|
LayoutPtr Layout::MakeFromXmlElement(const XMLElement* layoutElem)
|
||||||
{
|
{
|
||||||
auto layout = std::make_shared<Layout>();
|
const char* name = layoutElem->Attribute("name");
|
||||||
if (auto name = layoutElem->Attribute("name")) {
|
const char* id = layoutElem->Attribute("id");
|
||||||
layout->m_id = name;
|
|
||||||
layout->m_name = name;
|
if (id == nullptr || name == nullptr) {
|
||||||
|
LOG(WARNING, "Invalid XML layout provided\n");
|
||||||
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto layout = std::make_shared<Layout>();
|
||||||
|
layout->m_id = id;
|
||||||
|
layout->m_name = name;
|
||||||
layout->m_elem = layoutElem->DeepClone(&layout->m_dummyDoc)->ToElement();
|
layout->m_elem = layoutElem->DeepClone(&layout->m_dummyDoc)->ToElement();
|
||||||
|
|
||||||
|
ASSERT(!layout->m_name.empty() && !layout->m_id.empty());
|
||||||
|
|
||||||
|
if (layout->m_elem->ChildElementCount() == 0) // TODO: More error checking here.
|
||||||
|
return nullptr;
|
||||||
|
|
||||||
|
if (layout->m_name.empty() || layout->m_id.empty())
|
||||||
|
return nullptr;
|
||||||
|
|
||||||
return layout;
|
return layout;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -160,20 +176,22 @@ LayoutPtr Layout::MakeFromDock(const std::string& id, const std::string& name, c
|
||||||
layout->m_name = name;
|
layout->m_name = name;
|
||||||
|
|
||||||
layout->m_elem = layout->m_dummyDoc.NewElement("layout");
|
layout->m_elem = layout->m_dummyDoc.NewElement("layout");
|
||||||
|
layout->m_elem->SetAttribute("id", id.c_str());
|
||||||
layout->m_elem->SetAttribute("name", name.c_str());
|
layout->m_elem->SetAttribute("name", name.c_str());
|
||||||
save_dock_layout(layout->m_elem, dock);
|
save_dock_layout(layout->m_elem, dock);
|
||||||
|
|
||||||
return layout;
|
return layout;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Layout::matchId(const std::string& id) const
|
bool Layout::matchId(const std::string_view id) const
|
||||||
{
|
{
|
||||||
if (m_id == id)
|
if (m_id == id)
|
||||||
return true;
|
return true;
|
||||||
else if ((m_id.empty() && id == kDefault) || (m_id == kDefault && id.empty()))
|
|
||||||
|
if ((m_id.empty() && id == kDefault) || (m_id == kDefault && id.empty()))
|
||||||
return true;
|
return true;
|
||||||
else
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Layout::loadLayout(Dock* dock) const
|
bool Layout::loadLayout(Dock* dock) const
|
||||||
|
@ -190,4 +208,15 @@ bool Layout::loadLayout(Dock* dock) const
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool Layout::isValidName(const std::string_view name)
|
||||||
|
{
|
||||||
|
if (name.empty())
|
||||||
|
return false;
|
||||||
|
if (name[0] == '_')
|
||||||
|
return false;
|
||||||
|
if (name.length() > 128)
|
||||||
|
return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace app
|
} // namespace app
|
||||||
|
|
|
@ -24,6 +24,9 @@ public:
|
||||||
static constexpr const char* kDefault = "_default_";
|
static constexpr const char* kDefault = "_default_";
|
||||||
static constexpr const char* kMirroredDefault = "_mirrored_default_";
|
static constexpr const char* kMirroredDefault = "_mirrored_default_";
|
||||||
|
|
||||||
|
static constexpr const char* kDefaultOriginal = "_default_original_";
|
||||||
|
static constexpr const char* kMirroredDefaultOriginal = "_mirrored_default_original_";
|
||||||
|
|
||||||
static LayoutPtr MakeFromXmlElement(const tinyxml2::XMLElement* layoutElem);
|
static LayoutPtr MakeFromXmlElement(const tinyxml2::XMLElement* layoutElem);
|
||||||
static LayoutPtr MakeFromDock(const std::string& id, const std::string& name, const Dock* dock);
|
static LayoutPtr MakeFromDock(const std::string& id, const std::string& name, const Dock* dock);
|
||||||
|
|
||||||
|
@ -31,9 +34,14 @@ public:
|
||||||
const std::string& name() const { return m_name; }
|
const std::string& name() const { return m_name; }
|
||||||
const tinyxml2::XMLElement* xmlElement() const { return m_elem; }
|
const tinyxml2::XMLElement* xmlElement() const { return m_elem; }
|
||||||
|
|
||||||
bool matchId(const std::string& id) const;
|
bool matchId(std::string_view id) const;
|
||||||
bool loadLayout(Dock* dock) const;
|
bool loadLayout(Dock* dock) const;
|
||||||
|
|
||||||
|
bool isDefault() const { return m_id == kDefault || m_id == kMirroredDefault; }
|
||||||
|
|
||||||
|
// Validates that the given name is short and doesn't begin with a "_" (reserved for _defaults)
|
||||||
|
static bool isValidName(std::string_view name);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::string m_id;
|
std::string m_id;
|
||||||
std::string m_name;
|
std::string m_name;
|
||||||
|
|
|
@ -13,12 +13,15 @@
|
||||||
#include "app/app.h"
|
#include "app/app.h"
|
||||||
#include "app/i18n/strings.h"
|
#include "app/i18n/strings.h"
|
||||||
#include "app/match_words.h"
|
#include "app/match_words.h"
|
||||||
#include "app/ui/button_set.h"
|
#include "app/pref/preferences.h"
|
||||||
#include "app/ui/configure_timeline_popup.h"
|
|
||||||
#include "app/ui/main_window.h"
|
#include "app/ui/main_window.h"
|
||||||
#include "app/ui/separator_in_view.h"
|
#include "app/ui/separator_in_view.h"
|
||||||
#include "app/ui/skin/skin_theme.h"
|
#include "app/ui/skin/skin_theme.h"
|
||||||
|
#include "fmt/printf.h"
|
||||||
|
#include "ui/alert.h"
|
||||||
|
#include "ui/app_state.h"
|
||||||
#include "ui/entry.h"
|
#include "ui/entry.h"
|
||||||
|
#include "ui/label.h"
|
||||||
#include "ui/listitem.h"
|
#include "ui/listitem.h"
|
||||||
#include "ui/tooltips.h"
|
#include "ui/tooltips.h"
|
||||||
#include "ui/window.h"
|
#include "ui/window.h"
|
||||||
|
@ -34,57 +37,11 @@ using namespace ui;
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
// TODO Similar ButtonSet to the one in timeline_conf.xml
|
|
||||||
class TimelineButtons : public ButtonSet {
|
|
||||||
public:
|
|
||||||
TimelineButtons() : ButtonSet(2)
|
|
||||||
{
|
|
||||||
addItem(Strings::timeline_conf_left())->processMnemonicFromText();
|
|
||||||
addItem(Strings::timeline_conf_right())->processMnemonicFromText();
|
|
||||||
addItem(Strings::timeline_conf_bottom(), 2)->processMnemonicFromText();
|
|
||||||
|
|
||||||
auto& timelinePosOption = Preferences::instance().general.timelinePosition;
|
|
||||||
|
|
||||||
setSelectedButtonFromTimelinePosition(timelinePosOption());
|
|
||||||
m_timelinePosConn = timelinePosOption.AfterChange.connect(
|
|
||||||
[this](gen::TimelinePosition position) { setSelectedButtonFromTimelinePosition(position); });
|
|
||||||
|
|
||||||
InitTheme.connect([this] {
|
|
||||||
auto theme = skin::SkinTheme::get(this);
|
|
||||||
setStyle(theme->styles.separatorInView());
|
|
||||||
});
|
|
||||||
initTheme();
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
|
||||||
void setSelectedButtonFromTimelinePosition(gen::TimelinePosition pos)
|
|
||||||
{
|
|
||||||
int selItem = 0;
|
|
||||||
switch (pos) {
|
|
||||||
case gen::TimelinePosition::LEFT: selItem = 0; break;
|
|
||||||
case gen::TimelinePosition::RIGHT: selItem = 1; break;
|
|
||||||
case gen::TimelinePosition::BOTTOM: selItem = 2; break;
|
|
||||||
}
|
|
||||||
setSelectedItem(selItem, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
void onItemChange(Item* item) override
|
|
||||||
{
|
|
||||||
ButtonSet::onItemChange(item);
|
|
||||||
ConfigureTimelinePopup::onChangeTimelinePosition(selectedItem());
|
|
||||||
|
|
||||||
// Show the timeline
|
|
||||||
App::instance()->mainWindow()->setTimelineVisibility(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
obs::scoped_connection m_timelinePosConn;
|
|
||||||
};
|
|
||||||
|
|
||||||
// TODO this combobox is similar to FileSelector::CustomFileNameEntry
|
// TODO this combobox is similar to FileSelector::CustomFileNameEntry
|
||||||
// and GotoFrameCommand::TagsEntry
|
// and GotoFrameCommand::TagsEntry
|
||||||
class LayoutsEntry : public ComboBox {
|
class LayoutsEntry final : public ComboBox {
|
||||||
public:
|
public:
|
||||||
LayoutsEntry(Layouts& layouts) : m_layouts(layouts)
|
explicit LayoutsEntry(Layouts& layouts) : m_layouts(layouts)
|
||||||
{
|
{
|
||||||
setEditable(true);
|
setEditable(true);
|
||||||
getEntryWidget()->Change.connect(&LayoutsEntry::onEntryChange, this);
|
getEntryWidget()->Change.connect(&LayoutsEntry::onEntryChange, this);
|
||||||
|
@ -96,26 +53,32 @@ private:
|
||||||
{
|
{
|
||||||
deleteAllItems();
|
deleteAllItems();
|
||||||
|
|
||||||
MatchWords match(getEntryWidget()->text());
|
const MatchWords match(getEntryWidget()->text());
|
||||||
|
|
||||||
bool matchAny = false;
|
bool matchAny = false;
|
||||||
for (auto& layout : m_layouts) {
|
for (const auto& layout : m_layouts) {
|
||||||
|
if (layout->isDefault())
|
||||||
|
continue; // Ignore custom defaults.
|
||||||
|
|
||||||
if (match(layout->name())) {
|
if (match(layout->name())) {
|
||||||
matchAny = true;
|
matchAny = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (auto& layout : m_layouts) {
|
for (const auto& layout : m_layouts) {
|
||||||
|
if (layout->isDefault())
|
||||||
|
continue;
|
||||||
|
|
||||||
if (all || !matchAny || match(layout->name()))
|
if (all || !matchAny || match(layout->name()))
|
||||||
addItem(layout->name());
|
addItem(layout->name());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void onEntryChange()
|
void onEntryChange() override
|
||||||
{
|
{
|
||||||
closeListBox();
|
closeListBox();
|
||||||
fill(false);
|
fill(false);
|
||||||
if (getItemCount() > 0)
|
if (getItemCount() > 0 && !empty())
|
||||||
openListBox();
|
openListBox();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -124,9 +87,9 @@ private:
|
||||||
|
|
||||||
}; // namespace
|
}; // namespace
|
||||||
|
|
||||||
class LayoutSelector::LayoutItem : public ListItem {
|
class LayoutSelector::LayoutItem final : public ListItem {
|
||||||
public:
|
public:
|
||||||
enum LayoutOption {
|
enum LayoutOption : uint8_t {
|
||||||
DEFAULT,
|
DEFAULT,
|
||||||
MIRRORED_DEFAULT,
|
MIRRORED_DEFAULT,
|
||||||
USER_DEFINED,
|
USER_DEFINED,
|
||||||
|
@ -136,88 +99,210 @@ public:
|
||||||
LayoutItem(LayoutSelector* selector,
|
LayoutItem(LayoutSelector* selector,
|
||||||
const LayoutOption option,
|
const LayoutOption option,
|
||||||
const std::string& text,
|
const std::string& text,
|
||||||
const LayoutPtr layout)
|
const std::string& layoutId = "")
|
||||||
: ListItem(text)
|
: ListItem(text)
|
||||||
, m_option(option)
|
, m_option(option)
|
||||||
, m_selector(selector)
|
, m_selector(selector)
|
||||||
, m_layout(layout)
|
, m_layoutId(layoutId)
|
||||||
{
|
{
|
||||||
}
|
auto* hbox = new HBox;
|
||||||
|
hbox->setTransparent(true);
|
||||||
|
addChild(hbox);
|
||||||
|
|
||||||
std::string getLayoutId() const
|
auto* filler = new BoxFiller();
|
||||||
{
|
filler->setTransparent(true);
|
||||||
if (m_layout)
|
hbox->addChild(filler);
|
||||||
return m_layout->id();
|
|
||||||
else
|
|
||||||
return std::string();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool matchId(const std::string& id) const { return (m_layout && m_layout->matchId(id)); }
|
if (option == USER_DEFINED ||
|
||||||
|
((option == DEFAULT || option == MIRRORED_DEFAULT) && !layoutId.empty())) {
|
||||||
const LayoutPtr& layout() const { return m_layout; }
|
addActionButton();
|
||||||
|
|
||||||
void setLayout(const LayoutPtr& layout) { m_layout = layout; }
|
|
||||||
|
|
||||||
void selectImmediately()
|
|
||||||
{
|
|
||||||
MainWindow* win = App::instance()->mainWindow();
|
|
||||||
|
|
||||||
if (m_layout)
|
|
||||||
m_selector->m_activeLayoutId = m_layout->id();
|
|
||||||
|
|
||||||
switch (m_option) {
|
|
||||||
case LayoutOption::DEFAULT: win->setDefaultLayout(); break;
|
|
||||||
case LayoutOption::MIRRORED_DEFAULT: win->setMirroredDefaultLayout(); break;
|
|
||||||
}
|
}
|
||||||
// Even Default & Mirrored Default can have a customized layout
|
|
||||||
// (customized default layout).
|
|
||||||
if (m_layout)
|
|
||||||
win->loadUserLayout(m_layout.get());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void selectAfterClose()
|
// Separated from the constructor so we can add it on the fly when modifying Default/Mirrored
|
||||||
|
void addActionButton(const std::string& newLayoutId = "")
|
||||||
|
{
|
||||||
|
if (!newLayoutId.empty())
|
||||||
|
m_layoutId = newLayoutId;
|
||||||
|
|
||||||
|
ASSERT(!m_layoutId.empty());
|
||||||
|
|
||||||
|
// TODO: Custom icons for each one would be nice here.
|
||||||
|
m_actionButton = std::unique_ptr<IconButton>(
|
||||||
|
new IconButton(SkinTheme::instance()->parts.iconClose()));
|
||||||
|
m_actionButton->setSizeHint(
|
||||||
|
gfx::Size(m_actionButton->textHeight(), m_actionButton->textHeight()));
|
||||||
|
|
||||||
|
m_actionButton->setTransparent(true);
|
||||||
|
|
||||||
|
m_actionButton->InitTheme.connect(
|
||||||
|
[this] { m_actionButton->setBgColor(gfx::rgba(0, 0, 0, 0)); });
|
||||||
|
|
||||||
|
if (m_option == USER_DEFINED) {
|
||||||
|
m_actionConn = m_actionButton->Click.connect([this] {
|
||||||
|
const auto alert = Alert::create(Strings::new_layout_deleting_layout());
|
||||||
|
alert->addLabel(Strings::new_layout_deleting_layout_confirmation(text()), LEFT);
|
||||||
|
alert->addButton(Strings::general_ok());
|
||||||
|
alert->addButton(Strings::general_cancel());
|
||||||
|
if (alert->show() == 1) {
|
||||||
|
if (m_layoutId == m_selector->activeLayoutId()) {
|
||||||
|
m_selector->setActiveLayoutId(Layout::kDefault);
|
||||||
|
App::instance()->mainWindow()->setDefaultLayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
m_selector->removeLayout(m_layoutId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
m_actionConn = m_actionButton->Click.connect([this] {
|
||||||
|
const auto alert = Alert::create(Strings::new_layout_restoring_layout());
|
||||||
|
alert->addLabel(
|
||||||
|
Strings::new_layout_restoring_layout_confirmation(text().substr(0, text().size() - 1)),
|
||||||
|
LEFT);
|
||||||
|
alert->addButton(Strings::general_ok());
|
||||||
|
alert->addButton(Strings::general_cancel());
|
||||||
|
|
||||||
|
if (alert->show() == 1) {
|
||||||
|
if (m_layoutId == Layout::kDefault) {
|
||||||
|
App::instance()->mainWindow()->setDefaultLayout();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
App::instance()->mainWindow()->setMirroredDefaultLayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
m_selector->setActiveLayoutId(m_layoutId);
|
||||||
|
m_selector->removeLayout(m_layoutId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
children()[0]->addChild(m_actionButton.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string_view getLayoutId() const { return m_layoutId; }
|
||||||
|
|
||||||
|
void selectImmediately() const
|
||||||
{
|
{
|
||||||
MainWindow* win = App::instance()->mainWindow();
|
MainWindow* win = App::instance()->mainWindow();
|
||||||
|
|
||||||
switch (m_option) {
|
switch (m_option) {
|
||||||
case LayoutOption::NEW_LAYOUT: {
|
case DEFAULT: {
|
||||||
// Select the "Layout" separator (it's like selecting nothing)
|
if (const auto& defaultLayout = win->layoutSelector()->m_layouts.getById(
|
||||||
// TODO improve the ComboBox to select a real "nothing" (with
|
Layout::kDefault)) {
|
||||||
// a placeholder text)
|
win->loadUserLayout(defaultLayout.get());
|
||||||
m_selector->m_comboBox.setSelectedItemIndex(0);
|
|
||||||
|
|
||||||
gen::NewLayout window;
|
|
||||||
LayoutsEntry name(m_selector->m_layouts);
|
|
||||||
name.getEntryWidget()->setMaxTextLength(128);
|
|
||||||
name.setFocusMagnet(true);
|
|
||||||
name.setValue(Strings::new_layout_default_name(m_selector->m_layouts.size() + 1));
|
|
||||||
|
|
||||||
window.namePlaceholder()->addChild(&name);
|
|
||||||
window.openWindowInForeground();
|
|
||||||
if (window.closer() == window.ok()) {
|
|
||||||
auto layout =
|
|
||||||
Layout::MakeFromDock(name.getValue(), name.getValue(), win->customizableDock());
|
|
||||||
|
|
||||||
m_selector->addLayout(layout);
|
|
||||||
}
|
}
|
||||||
break;
|
else {
|
||||||
|
win->setDefaultLayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
m_selector->setActiveLayoutId(Layout::kDefault);
|
||||||
|
} break;
|
||||||
|
case MIRRORED_DEFAULT: {
|
||||||
|
if (const auto& mirroredLayout = win->layoutSelector()->m_layouts.getById(
|
||||||
|
Layout::kMirroredDefault)) {
|
||||||
|
win->loadUserLayout(mirroredLayout.get());
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
win->setMirroredDefaultLayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
m_selector->setActiveLayoutId(Layout::kMirroredDefault);
|
||||||
|
} break;
|
||||||
|
case USER_DEFINED: {
|
||||||
|
const auto selectedLayout = m_selector->m_layouts.getById(m_layoutId);
|
||||||
|
ASSERT(!m_layoutId.empty());
|
||||||
|
ASSERT(selectedLayout);
|
||||||
|
m_selector->setActiveLayoutId(m_layoutId);
|
||||||
|
win->loadUserLayout(selectedLayout.get());
|
||||||
|
} break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void selectAfterClose() const
|
||||||
|
{
|
||||||
|
if (m_option != NEW_LAYOUT)
|
||||||
|
return;
|
||||||
|
|
||||||
|
//
|
||||||
|
// Adding a NEW_LAYOUT
|
||||||
|
//
|
||||||
|
MainWindow* win = App::instance()->mainWindow();
|
||||||
|
gen::NewLayout window;
|
||||||
|
|
||||||
|
if (m_selector->m_layouts.size() > 0)
|
||||||
|
window.base()->addItem(new SeparatorInView());
|
||||||
|
|
||||||
|
// Sort the layouts by putting the defaults first, in case the user made a custom new one before
|
||||||
|
// modifying a default.
|
||||||
|
constexpr struct {
|
||||||
|
bool operator()(LayoutPtr& a, LayoutPtr& b) const { return a->isDefault(); }
|
||||||
|
} customDefaultSort;
|
||||||
|
std::sort(m_selector->m_layouts.begin(), m_selector->m_layouts.end(), customDefaultSort);
|
||||||
|
|
||||||
|
for (const auto& layout : m_selector->m_layouts) {
|
||||||
|
ListItem* item;
|
||||||
|
if (layout->isDefault()) {
|
||||||
|
item = new ListItem(Strings::new_layout_modified(
|
||||||
|
layout->id() == Layout::kDefault ? Strings::main_window_default_layout() :
|
||||||
|
Strings::main_window_mirrored_default_layout()));
|
||||||
}
|
}
|
||||||
default:
|
else {
|
||||||
// Do nothing
|
item = new ListItem(layout->name());
|
||||||
break;
|
}
|
||||||
|
|
||||||
|
item->setValue(layout->id());
|
||||||
|
window.base()->addItem(item);
|
||||||
|
|
||||||
|
if (m_selector->m_activeLayoutId == layout->id())
|
||||||
|
window.base()->setSelectedItemIndex(window.base()->getItemCount() - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.name()->Change.connect([&] {
|
||||||
|
bool valid = Layout::isValidName(window.name()->text()) &&
|
||||||
|
m_selector->m_layouts.getById(window.name()->text()) == nullptr;
|
||||||
|
window.ok()->setEnabled(valid);
|
||||||
|
});
|
||||||
|
|
||||||
|
window.openWindowInForeground();
|
||||||
|
|
||||||
|
if (window.closer() == window.ok()) {
|
||||||
|
if (window.base()->getValue() == Layout::kDefaultOriginal)
|
||||||
|
win->setDefaultLayout();
|
||||||
|
else if (window.base()->getValue() == Layout::kMirroredDefaultOriginal)
|
||||||
|
win->setMirroredDefaultLayout();
|
||||||
|
else {
|
||||||
|
const auto baseLayout = m_selector->m_layouts.getById(window.base()->getValue());
|
||||||
|
ASSERT(baseLayout);
|
||||||
|
win->loadUserLayout(baseLayout.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto layout =
|
||||||
|
Layout::MakeFromDock(window.name()->text(), window.name()->text(), win->customizableDock());
|
||||||
|
|
||||||
|
m_selector->addLayout(layout);
|
||||||
|
m_selector->m_layouts.saveUserLayouts();
|
||||||
|
m_selector->setActiveLayoutId(layout->id());
|
||||||
|
win->loadUserLayout(layout.get());
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Ensure we go back to having the layout we were at selected.
|
||||||
|
m_selector->populateComboBox();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
LayoutOption m_option;
|
LayoutOption m_option;
|
||||||
LayoutSelector* m_selector;
|
LayoutSelector* m_selector;
|
||||||
LayoutPtr m_layout;
|
std::string m_layoutId;
|
||||||
|
std::unique_ptr<IconButton> m_actionButton;
|
||||||
|
obs::scoped_connection m_actionConn;
|
||||||
};
|
};
|
||||||
|
|
||||||
void LayoutSelector::LayoutComboBox::onChange()
|
void LayoutSelector::LayoutComboBox::onChange()
|
||||||
{
|
{
|
||||||
ComboBox::onChange();
|
ComboBox::onChange();
|
||||||
if (auto item = dynamic_cast<LayoutItem*>(getSelectedItem())) {
|
if (auto* item = dynamic_cast<LayoutItem*>(getSelectedItem())) {
|
||||||
item->selectImmediately();
|
item->selectImmediately();
|
||||||
m_selected = item;
|
m_selected = item;
|
||||||
}
|
}
|
||||||
|
@ -235,7 +320,7 @@ void LayoutSelector::LayoutComboBox::onCloseListBox()
|
||||||
LayoutSelector::LayoutSelector(TooltipManager* tooltipManager)
|
LayoutSelector::LayoutSelector(TooltipManager* tooltipManager)
|
||||||
: m_button(SkinTheme::instance()->parts.iconUserData())
|
: m_button(SkinTheme::instance()->parts.iconUserData())
|
||||||
{
|
{
|
||||||
m_activeLayoutId = Preferences::instance().general.workspaceLayout();
|
setActiveLayoutId(Preferences::instance().general.workspaceLayout());
|
||||||
|
|
||||||
m_button.Click.connect([this]() { switchSelector(); });
|
m_button.Click.connect([this]() { switchSelector(); });
|
||||||
|
|
||||||
|
@ -258,44 +343,55 @@ LayoutSelector::~LayoutSelector()
|
||||||
{
|
{
|
||||||
Preferences::instance().general.workspaceLayout(m_activeLayoutId);
|
Preferences::instance().general.workspaceLayout(m_activeLayoutId);
|
||||||
|
|
||||||
stopAnimation();
|
if (!is_app_state_closing())
|
||||||
|
stopAnimation();
|
||||||
}
|
}
|
||||||
|
|
||||||
LayoutPtr LayoutSelector::activeLayout()
|
LayoutPtr LayoutSelector::activeLayout() const
|
||||||
{
|
{
|
||||||
return m_layouts.getById(m_activeLayoutId);
|
return m_layouts.getById(m_activeLayoutId);
|
||||||
}
|
}
|
||||||
|
|
||||||
void LayoutSelector::addLayout(const LayoutPtr& layout)
|
void LayoutSelector::addLayout(const LayoutPtr& layout)
|
||||||
{
|
{
|
||||||
bool added = m_layouts.addLayout(layout);
|
m_layouts.addLayout(layout);
|
||||||
if (added) {
|
|
||||||
auto item = new LayoutItem(this, LayoutItem::USER_DEFINED, layout->name(), layout);
|
|
||||||
m_comboBox.insertItem(m_comboBox.getItemCount() - 1, // Above the "New Layout" item
|
|
||||||
item);
|
|
||||||
|
|
||||||
m_comboBox.setSelectedItem(item);
|
// HACK: Because this function is called from inside a LayoutItem, clearing the combobox items
|
||||||
}
|
// will crash.
|
||||||
else {
|
// TODO: Is there a better way to do this?
|
||||||
for (auto item : m_comboBox) {
|
auto* msg = new CallbackMessage([this] { populateComboBox(); });
|
||||||
if (auto layoutItem = dynamic_cast<LayoutItem*>(item)) {
|
msg->setRecipient(this);
|
||||||
if (layoutItem->layout() && layoutItem->layout()->name() == layout->name()) {
|
manager()->enqueueMessage(msg);
|
||||||
layoutItem->setLayout(layout);
|
}
|
||||||
m_comboBox.setSelectedItem(item);
|
|
||||||
break;
|
void LayoutSelector::removeLayout(const LayoutPtr& layout)
|
||||||
}
|
{
|
||||||
}
|
m_layouts.removeLayout(layout);
|
||||||
}
|
m_layouts.saveUserLayouts();
|
||||||
}
|
|
||||||
|
// TODO: See addLayout
|
||||||
|
auto* msg = new CallbackMessage([this] { populateComboBox(); });
|
||||||
|
msg->setRecipient(this);
|
||||||
|
manager()->enqueueMessage(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
void LayoutSelector::removeLayout(const std::string& layoutId)
|
||||||
|
{
|
||||||
|
auto layout = m_layouts.getById(layoutId);
|
||||||
|
ASSERT(layout);
|
||||||
|
removeLayout(layout);
|
||||||
}
|
}
|
||||||
|
|
||||||
void LayoutSelector::updateActiveLayout(const LayoutPtr& newLayout)
|
void LayoutSelector::updateActiveLayout(const LayoutPtr& newLayout)
|
||||||
{
|
{
|
||||||
bool result = m_layouts.addLayout(newLayout);
|
bool added = m_layouts.addLayout(newLayout);
|
||||||
|
setActiveLayoutId(newLayout->id());
|
||||||
|
m_layouts.saveUserLayouts();
|
||||||
|
|
||||||
// It means that the layout wasn't added, but replaced, when we
|
if (added && newLayout->isDefault()) {
|
||||||
// update a layout it must be existent in the m_layouts collection.
|
// Mark it with an asterisk if we're editing a default layout.
|
||||||
ASSERT(result == false);
|
populateComboBox();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void LayoutSelector::onAnimationFrame()
|
void LayoutSelector::onAnimationFrame()
|
||||||
|
@ -311,7 +407,7 @@ void LayoutSelector::onAnimationFrame()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (auto win = window())
|
if (auto* win = window())
|
||||||
win->layout();
|
win->layout();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -335,7 +431,7 @@ void LayoutSelector::onAnimationStop(int animation)
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (auto win = window())
|
if (auto* win = window())
|
||||||
win->layout();
|
win->layout();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -347,23 +443,7 @@ void LayoutSelector::switchSelector()
|
||||||
|
|
||||||
// Create the combobox for first time
|
// Create the combobox for first time
|
||||||
if (m_comboBox.getItemCount() == 0) {
|
if (m_comboBox.getItemCount() == 0) {
|
||||||
m_comboBox.addItem(new SeparatorInView(Strings::main_window_layout(), HORIZONTAL));
|
populateComboBox();
|
||||||
m_comboBox.addItem(new LayoutItem(this,
|
|
||||||
LayoutItem::DEFAULT,
|
|
||||||
Strings::main_window_default_layout(),
|
|
||||||
m_layouts.getById(Layout::kDefault)));
|
|
||||||
m_comboBox.addItem(new LayoutItem(this,
|
|
||||||
LayoutItem::MIRRORED_DEFAULT,
|
|
||||||
Strings::main_window_mirrored_default_layout(),
|
|
||||||
m_layouts.getById(Layout::kMirroredDefault)));
|
|
||||||
m_comboBox.addItem(new SeparatorInView(Strings::main_window_timeline(), HORIZONTAL));
|
|
||||||
m_comboBox.addItem(new TimelineButtons());
|
|
||||||
m_comboBox.addItem(new SeparatorInView(Strings::main_window_user_layouts(), HORIZONTAL));
|
|
||||||
for (const auto& layout : m_layouts) {
|
|
||||||
m_comboBox.addItem(new LayoutItem(this, LayoutItem::USER_DEFINED, layout->name(), layout));
|
|
||||||
}
|
|
||||||
m_comboBox.addItem(
|
|
||||||
new LayoutItem(this, LayoutItem::NEW_LAYOUT, Strings::main_window_new_layout(), nullptr));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
m_comboBox.setVisible(true);
|
m_comboBox.setVisible(true);
|
||||||
|
@ -377,7 +457,7 @@ void LayoutSelector::switchSelector()
|
||||||
m_endSize = gfx::Size(0, 0);
|
m_endSize = gfx::Size(0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (auto item = getItemByLayoutId(m_activeLayoutId))
|
if (auto* item = getItemByLayoutId(m_activeLayoutId))
|
||||||
m_comboBox.setSelectedItem(item);
|
m_comboBox.setSelectedItem(item);
|
||||||
|
|
||||||
m_comboBox.setSizeHint(m_startSize);
|
m_comboBox.setSizeHint(m_startSize);
|
||||||
|
@ -403,14 +483,53 @@ void LayoutSelector::setupTooltips(TooltipManager* tooltipManager)
|
||||||
tooltipManager->addTooltipFor(&m_button, Strings::main_window_layout(), TOP);
|
tooltipManager->addTooltipFor(&m_button, Strings::main_window_layout(), TOP);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void LayoutSelector::populateComboBox()
|
||||||
|
{
|
||||||
|
m_comboBox.deleteAllItems();
|
||||||
|
|
||||||
|
m_comboBox.addItem(new SeparatorInView(Strings::main_window_layout(), HORIZONTAL));
|
||||||
|
m_comboBox.addItem(
|
||||||
|
new LayoutItem(this, LayoutItem::DEFAULT, Strings::main_window_default_layout()));
|
||||||
|
m_comboBox.addItem(new LayoutItem(this,
|
||||||
|
LayoutItem::MIRRORED_DEFAULT,
|
||||||
|
Strings::main_window_mirrored_default_layout()));
|
||||||
|
m_comboBox.addItem(new SeparatorInView(Strings::main_window_user_layouts(), HORIZONTAL));
|
||||||
|
for (const auto& layout : m_layouts) {
|
||||||
|
LayoutItem* item;
|
||||||
|
if (layout->isDefault()) {
|
||||||
|
item = dynamic_cast<LayoutItem*>(
|
||||||
|
m_comboBox.getItem(layout->id() == Layout::kDefault ? 1 : 2));
|
||||||
|
// Indicate we've modified this with an asterisk.
|
||||||
|
item->setText(item->text() + "*");
|
||||||
|
item->addActionButton(layout->id());
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
item = new LayoutItem(this, LayoutItem::USER_DEFINED, layout->name(), layout->id());
|
||||||
|
m_comboBox.addItem(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (layout->id() == m_activeLayoutId)
|
||||||
|
m_comboBox.setSelectedItem(item);
|
||||||
|
}
|
||||||
|
m_comboBox.addItem(
|
||||||
|
new LayoutItem(this, LayoutItem::NEW_LAYOUT, Strings::main_window_new_layout(), ""));
|
||||||
|
|
||||||
|
if (m_activeLayoutId == Layout::kDefault)
|
||||||
|
m_comboBox.setSelectedItemIndex(1);
|
||||||
|
if (m_activeLayoutId == Layout::kMirroredDefault)
|
||||||
|
m_comboBox.setSelectedItemIndex(2);
|
||||||
|
|
||||||
|
m_comboBox.getEntryWidget()->deselectText();
|
||||||
|
}
|
||||||
LayoutSelector::LayoutItem* LayoutSelector::getItemByLayoutId(const std::string& id)
|
LayoutSelector::LayoutItem* LayoutSelector::getItemByLayoutId(const std::string& id)
|
||||||
{
|
{
|
||||||
for (auto child : m_comboBox) {
|
for (auto* child : m_comboBox) {
|
||||||
if (auto item = dynamic_cast<LayoutItem*>(child)) {
|
if (auto* item = dynamic_cast<LayoutItem*>(child)) {
|
||||||
if (item->matchId(id))
|
if (item->getLayoutId() == id)
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -47,10 +47,12 @@ public:
|
||||||
LayoutSelector(ui::TooltipManager* tooltipManager);
|
LayoutSelector(ui::TooltipManager* tooltipManager);
|
||||||
~LayoutSelector();
|
~LayoutSelector();
|
||||||
|
|
||||||
LayoutPtr activeLayout();
|
LayoutPtr activeLayout() const;
|
||||||
std::string activeLayoutId() const { return m_activeLayoutId; }
|
const std::string& activeLayoutId() const { return m_activeLayoutId; }
|
||||||
|
|
||||||
void addLayout(const LayoutPtr& layout);
|
void addLayout(const LayoutPtr& layout);
|
||||||
|
void removeLayout(const LayoutPtr& layout);
|
||||||
|
void removeLayout(const std::string& layoutId);
|
||||||
void updateActiveLayout(const LayoutPtr& layout);
|
void updateActiveLayout(const LayoutPtr& layout);
|
||||||
void switchSelector();
|
void switchSelector();
|
||||||
void switchSelectorFromCommand();
|
void switchSelectorFromCommand();
|
||||||
|
@ -61,6 +63,20 @@ public:
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void setupTooltips(ui::TooltipManager* tooltipManager);
|
void setupTooltips(ui::TooltipManager* tooltipManager);
|
||||||
|
void setActiveLayoutId(const std::string& layoutId)
|
||||||
|
{
|
||||||
|
if (layoutId.empty()) {
|
||||||
|
m_activeLayoutId = Layout::kDefault;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (layoutId == m_activeLayoutId)
|
||||||
|
return;
|
||||||
|
|
||||||
|
m_activeLayoutId = layoutId;
|
||||||
|
}
|
||||||
|
|
||||||
|
void populateComboBox();
|
||||||
LayoutItem* getItemByLayoutId(const std::string& id);
|
LayoutItem* getItemByLayoutId(const std::string& id);
|
||||||
void onAnimationFrame() override;
|
void onAnimationFrame() override;
|
||||||
void onAnimationStop(int animation) override;
|
void onAnimationStop(int animation) override;
|
||||||
|
|
|
@ -36,8 +36,12 @@ Layouts::Layouts()
|
||||||
|
|
||||||
Layouts::~Layouts()
|
Layouts::~Layouts()
|
||||||
{
|
{
|
||||||
if (!m_userLayoutsFilename.empty())
|
try {
|
||||||
save(m_userLayoutsFilename);
|
saveUserLayouts();
|
||||||
|
}
|
||||||
|
catch (const std::exception& ex) {
|
||||||
|
LOG(ERROR, "LAY: Error saving user layouts on exit: %s\n", ex.what());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LayoutPtr Layouts::getById(const std::string& id) const
|
LayoutPtr Layouts::getById(const std::string& id) const
|
||||||
|
@ -50,28 +54,69 @@ LayoutPtr Layouts::getById(const std::string& id) const
|
||||||
|
|
||||||
bool Layouts::addLayout(const LayoutPtr& layout)
|
bool Layouts::addLayout(const LayoutPtr& layout)
|
||||||
{
|
{
|
||||||
auto it = std::find_if(m_layouts.begin(), m_layouts.end(), [layout](const LayoutPtr& l) {
|
ASSERT(layout);
|
||||||
|
|
||||||
|
const auto it = std::find_if(m_layouts.begin(), m_layouts.end(), [layout](const LayoutPtr& l) {
|
||||||
return l->matchId(layout->id());
|
return l->matchId(layout->id());
|
||||||
});
|
});
|
||||||
|
|
||||||
if (it != m_layouts.end()) {
|
if (it != m_layouts.end()) {
|
||||||
*it = layout; // Replace existent layout
|
*it = layout; // Replace existent layout
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
m_layouts.push_back(layout);
|
m_layouts.push_back(layout);
|
||||||
return true;
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Layouts::removeLayout(const LayoutPtr& layout)
|
||||||
|
{
|
||||||
|
if (m_layouts.size() <= 1) {
|
||||||
|
m_layouts.clear();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ASSERT(layout);
|
||||||
|
|
||||||
|
const auto it = std::find_if(m_layouts.begin(), m_layouts.end(), [layout](const LayoutPtr& l) {
|
||||||
|
return l->matchId(layout->id());
|
||||||
|
});
|
||||||
|
|
||||||
|
m_layouts.erase(it);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Layouts::saveUserLayouts()
|
||||||
|
{
|
||||||
|
if (m_userLayoutsFilename.empty())
|
||||||
|
return;
|
||||||
|
|
||||||
|
save(m_userLayoutsFilename);
|
||||||
|
|
||||||
|
// TODO: We probably have too much I/O here, but it's the easiest way to keep the XML and
|
||||||
|
// internal representations synced up.
|
||||||
|
reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Layouts::reload()
|
||||||
|
{
|
||||||
|
if (m_userLayoutsFilename.empty())
|
||||||
|
return;
|
||||||
|
|
||||||
|
m_layouts.clear();
|
||||||
|
load(m_userLayoutsFilename);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Layouts::load(const std::string& fn)
|
void Layouts::load(const std::string& fn)
|
||||||
{
|
{
|
||||||
XMLDocumentRef doc = app::open_xml(fn);
|
const XMLDocumentRef doc = app::open_xml(fn);
|
||||||
XMLHandle handle(doc.get());
|
XMLHandle handle(doc.get());
|
||||||
XMLElement* layoutElem =
|
XMLElement* layoutElem =
|
||||||
handle.FirstChildElement("layouts").FirstChildElement("layout").ToElement();
|
handle.FirstChildElement("layouts").FirstChildElement("layout").ToElement();
|
||||||
|
|
||||||
while (layoutElem) {
|
while (layoutElem) {
|
||||||
m_layouts.push_back(Layout::MakeFromXmlElement(layoutElem));
|
if (auto layout = Layout::MakeFromXmlElement(layoutElem)) {
|
||||||
|
m_layouts.push_back(layout);
|
||||||
|
}
|
||||||
layoutElem = layoutElem->NextSiblingElement();
|
layoutElem = layoutElem->NextSiblingElement();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -85,7 +130,7 @@ void Layouts::save(const std::string& fn) const
|
||||||
layoutsElem->InsertEndChild(layout->xmlElement()->DeepClone(doc.get()));
|
layoutsElem->InsertEndChild(layout->xmlElement()->DeepClone(doc.get()));
|
||||||
}
|
}
|
||||||
|
|
||||||
doc->InsertEndChild(doc->NewDeclaration("xml version=\"1.0\" encoding=\"utf-8\""));
|
doc->InsertEndChild(doc->NewDeclaration(R"(xml version="1.0" encoding="utf-8")"));
|
||||||
doc->InsertEndChild(layoutsElem);
|
doc->InsertEndChild(layoutsElem);
|
||||||
save_xml(doc.get(), fn);
|
save_xml(doc.get(), fn);
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,6 +27,10 @@ public:
|
||||||
// Returns true if the layout is added, or false if it was
|
// Returns true if the layout is added, or false if it was
|
||||||
// replaced.
|
// replaced.
|
||||||
bool addLayout(const LayoutPtr& layout);
|
bool addLayout(const LayoutPtr& layout);
|
||||||
|
void removeLayout(const LayoutPtr& layout);
|
||||||
|
|
||||||
|
void saveUserLayouts();
|
||||||
|
void reload();
|
||||||
|
|
||||||
// To iterate layouts
|
// To iterate layouts
|
||||||
using List = std::vector<LayoutPtr>;
|
using List = std::vector<LayoutPtr>;
|
||||||
|
|
|
@ -60,9 +60,9 @@ namespace app {
|
||||||
|
|
||||||
using namespace ui;
|
using namespace ui;
|
||||||
|
|
||||||
static const char* kLegacyLayoutMainWindowSection = "layout:main_window";
|
static constexpr const char* kLegacyLayoutMainWindowSection = "layout:main_window";
|
||||||
static const char* kLegacyLayoutTimelineSplitter = "timeline_splitter";
|
static constexpr const char* kLegacyLayoutTimelineSplitter = "timeline_splitter";
|
||||||
static const char* kLegacyLayoutColorBarSplitter = "color_bar_splitter";
|
static constexpr const char* kLegacyLayoutColorBarSplitter = "color_bar_splitter";
|
||||||
|
|
||||||
class ScreenScalePanic : public INotificationDelegate {
|
class ScreenScalePanic : public INotificationDelegate {
|
||||||
public:
|
public:
|
||||||
|
@ -185,8 +185,8 @@ void MainWindow::initialize()
|
||||||
m_dock->dock(ui::CENTER, m_customizableDockPlaceholder.get());
|
m_dock->dock(ui::CENTER, m_customizableDockPlaceholder.get());
|
||||||
|
|
||||||
// After the user resizes the dock we save the updated layout
|
// After the user resizes the dock we save the updated layout
|
||||||
m_saveDockLayoutConn = m_customizableDock->UserResizedDock.connect(
|
m_saveDockLayoutConn = m_customizableDock->UserResizedDock.connect(&MainWindow::saveActiveLayout,
|
||||||
[this] { saveActiveLayout(); });
|
this);
|
||||||
|
|
||||||
setDefaultLayout();
|
setDefaultLayout();
|
||||||
if (LayoutPtr layout = m_layoutSelector->activeLayout())
|
if (LayoutPtr layout = m_layoutSelector->activeLayout())
|
||||||
|
@ -202,7 +202,7 @@ void MainWindow::initialize()
|
||||||
|
|
||||||
AppMenus::instance()->rebuildRecentList();
|
AppMenus::instance()->rebuildRecentList();
|
||||||
|
|
||||||
// When the language is change, we reload the menu bar strings and
|
// When the language is changed, we reload the menu bar strings and
|
||||||
// relayout the whole main window.
|
// relayout the whole main window.
|
||||||
Strings::instance()->LanguageChange.connect([this] { onLanguageChange(); });
|
Strings::instance()->LanguageChange.connect([this] { onLanguageChange(); });
|
||||||
}
|
}
|
||||||
|
@ -216,6 +216,10 @@ MainWindow::~MainWindow()
|
||||||
m_dock->resetDocks();
|
m_dock->resetDocks();
|
||||||
m_customizableDock->resetDocks();
|
m_customizableDock->resetDocks();
|
||||||
|
|
||||||
|
// Leaving them in can cause crashes when cleaning up.
|
||||||
|
m_dock = nullptr;
|
||||||
|
m_customizableDock = nullptr;
|
||||||
|
|
||||||
m_layoutSelector.reset();
|
m_layoutSelector.reset();
|
||||||
m_scalePanic.reset();
|
m_scalePanic.reset();
|
||||||
|
|
||||||
|
@ -386,9 +390,7 @@ void MainWindow::setTimelineVisibility(bool visible)
|
||||||
|
|
||||||
void MainWindow::popTimeline()
|
void MainWindow::popTimeline()
|
||||||
{
|
{
|
||||||
Preferences& preferences = Preferences::instance();
|
if (!Preferences::instance().general.autoshowTimeline())
|
||||||
|
|
||||||
if (!preferences.general.autoshowTimeline())
|
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (!getTimelineVisibility())
|
if (!getTimelineVisibility())
|
||||||
|
@ -400,18 +402,42 @@ void MainWindow::setDefaultLayout()
|
||||||
m_timelineResizeConn.disconnect();
|
m_timelineResizeConn.disconnect();
|
||||||
m_colorBarResizeConn.disconnect();
|
m_colorBarResizeConn.disconnect();
|
||||||
|
|
||||||
auto colorBarWidth = get_config_double(kLegacyLayoutMainWindowSection,
|
const auto colorBarWidth = get_config_double(kLegacyLayoutMainWindowSection,
|
||||||
kLegacyLayoutColorBarSplitter,
|
kLegacyLayoutColorBarSplitter,
|
||||||
m_colorBar->sizeHint().w);
|
m_colorBar->sizeHint().w);
|
||||||
|
|
||||||
m_customizableDock->resetDocks();
|
m_customizableDock->resetDocks();
|
||||||
m_customizableDock->dock(ui::LEFT, m_colorBar.get(), gfx::Size(colorBarWidth, 0));
|
m_customizableDock->dock(ui::LEFT, m_colorBar.get(), gfx::Size(colorBarWidth, 0));
|
||||||
m_customizableDock->dock(ui::BOTTOM, m_statusBar.get());
|
m_customizableDock->dock(ui::BOTTOM, m_statusBar.get());
|
||||||
m_customizableDock->center()->dock(ui::TOP, m_contextBar.get());
|
m_customizableDock->center()->dock(ui::TOP, m_contextBar.get());
|
||||||
m_customizableDock->center()->dock(ui::RIGHT, m_toolBar.get());
|
m_customizableDock->center()->dock(ui::RIGHT, m_toolBar.get());
|
||||||
m_customizableDock->center()->center()->dock(ui::BOTTOM,
|
|
||||||
|
const auto timelineSplitterPos =
|
||||||
|
get_config_double(kLegacyLayoutMainWindowSection, kLegacyLayoutTimelineSplitter, 75.0) / 100.0;
|
||||||
|
const auto timelinePos = Preferences::instance().general.timelinePosition();
|
||||||
|
const auto workspaceBounds = m_workspace->bounds();
|
||||||
|
|
||||||
|
int timelineSide;
|
||||||
|
switch (timelinePos) {
|
||||||
|
case gen::TimelinePosition::LEFT: timelineSide = ui::LEFT; break;
|
||||||
|
case gen::TimelinePosition::RIGHT: timelineSide = ui::LEFT; break;
|
||||||
|
default:
|
||||||
|
case gen::TimelinePosition::BOTTOM: timelineSide = ui::BOTTOM; break;
|
||||||
|
}
|
||||||
|
|
||||||
|
gfx::Size timelineSize(75, 75);
|
||||||
|
if ((timelineSide & RIGHT) || (timelineSide & LEFT)) {
|
||||||
|
timelineSize.w = (workspaceBounds.w * (1.0 - timelineSplitterPos)) / guiscale();
|
||||||
|
}
|
||||||
|
if ((timelineSide & BOTTOM) || (timelineSide & TOP)) {
|
||||||
|
timelineSize.h = (workspaceBounds.h * (1.0 - timelineSplitterPos)) / guiscale();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Timeline config
|
||||||
|
m_customizableDock->center()->center()->dock(timelineSide,
|
||||||
m_timeline.get(),
|
m_timeline.get(),
|
||||||
gfx::Size(64 * guiscale(), 64 * guiscale()));
|
timelineSize.createUnion(gfx::Size(64, 64)));
|
||||||
|
|
||||||
m_customizableDock->center()->center()->dock(ui::CENTER, m_workspace.get());
|
m_customizableDock->center()->center()->dock(ui::CENTER, m_workspace.get());
|
||||||
configureWorkspaceLayout();
|
configureWorkspaceLayout();
|
||||||
}
|
}
|
||||||
|
@ -444,8 +470,10 @@ void MainWindow::loadUserLayout(const Layout* layout)
|
||||||
|
|
||||||
m_customizableDock->resetDocks();
|
m_customizableDock->resetDocks();
|
||||||
|
|
||||||
if (!layout->loadLayout(m_customizableDock))
|
if (!layout->loadLayout(m_customizableDock)) {
|
||||||
|
LOG(WARNING, "Layout %s failed to load, resetting to default.\n", layout->id().c_str());
|
||||||
setDefaultLayout();
|
setDefaultLayout();
|
||||||
|
}
|
||||||
|
|
||||||
this->layout();
|
this->layout();
|
||||||
}
|
}
|
||||||
|
@ -510,7 +538,7 @@ void MainWindow::onActiveViewChange()
|
||||||
{
|
{
|
||||||
// If we are closing the app, we just ignore all view changes (as
|
// If we are closing the app, we just ignore all view changes (as
|
||||||
// docs will be destroyed and views closed).
|
// docs will be destroyed and views closed).
|
||||||
if (get_app_state() != AppState::kNormal)
|
if (get_app_state() != AppState::kNormal || !m_dock)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// First we have to configure the MainWindow layout (e.g. show
|
// First we have to configure the MainWindow layout (e.g. show
|
||||||
|
@ -689,68 +717,44 @@ void MainWindow::configureWorkspaceLayout()
|
||||||
|
|
||||||
if (os::System::instance()->menus() == nullptr || pref.general.showMenuBar()) {
|
if (os::System::instance()->menus() == nullptr || pref.general.showMenuBar()) {
|
||||||
if (!m_menuBar->parent())
|
if (!m_menuBar->parent())
|
||||||
m_dock->top()->dock(CENTER, m_menuBar.get());
|
m_dock->top()->dock(ui::CENTER, m_menuBar.get());
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
if (m_menuBar->parent())
|
if (m_menuBar->parent()) {
|
||||||
m_dock->undock(m_menuBar.get());
|
m_dock->undock(m_dock->top());
|
||||||
|
m_dock->top()->resetDocks();
|
||||||
|
|
||||||
|
// TODO: I've tried a dozen different ways but I cannot get this combination to dock well
|
||||||
|
// without running into sizing problems for the notifications & selector buttons.
|
||||||
|
|
||||||
|
if (m_tabsBar)
|
||||||
|
m_dock->top()->dock(ui::CENTER, m_tabsBar.get());
|
||||||
|
|
||||||
|
if (m_notifications)
|
||||||
|
m_dock->top()->right()->dock(ui::CENTER, m_notifications.get());
|
||||||
|
|
||||||
|
if (m_layoutSelector)
|
||||||
|
m_dock->top()->right()->dock(ui::RIGHT, m_layoutSelector.get());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
m_menuBar->setVisible(normal);
|
m_menuBar->setVisible(normal);
|
||||||
m_notifications->setVisible(normal && m_notifications->hasNotifications());
|
m_notifications->setVisible(normal && m_notifications->hasNotifications());
|
||||||
m_tabsBar->setVisible(normal);
|
m_tabsBar->setVisible(normal);
|
||||||
|
|
||||||
// TODO set visibility of color bar widgets
|
|
||||||
m_colorBar->setVisible(normal && isDoc);
|
m_colorBar->setVisible(normal && isDoc);
|
||||||
m_colorBarResizeConn = m_customizableDock->Resize.connect(
|
m_colorBarResizeConn = m_customizableDock->Resize.connect(&MainWindow::saveColorBarConfiguration,
|
||||||
[this] { saveColorBarConfiguration(); });
|
this);
|
||||||
|
|
||||||
m_toolBar->setVisible(normal && isDoc);
|
m_toolBar->setVisible(normal && isDoc);
|
||||||
m_statusBar->setVisible(normal);
|
m_statusBar->setVisible(normal);
|
||||||
m_contextBar->setVisible(isDoc && (m_mode == NormalMode || m_mode == ContextBarAndTimelineMode));
|
m_contextBar->setVisible(isDoc && (m_mode == NormalMode || m_mode == ContextBarAndTimelineMode));
|
||||||
|
|
||||||
// Configure timeline
|
// Configure timeline
|
||||||
{
|
if (m_timeline && m_timeline->parent())
|
||||||
const gfx::Rect workspaceBounds = m_customizableDock->center()->center()->bounds();
|
m_timelineResizeConn = dynamic_cast<Dock*>(m_timeline->parent())
|
||||||
// Get legacy timeline position and splitter position
|
->Resize.connect(&MainWindow::saveTimelineConfiguration, this);
|
||||||
auto timelinePosition = pref.general.timelinePosition();
|
|
||||||
auto timelineSplitterPos =
|
|
||||||
get_config_double(kLegacyLayoutMainWindowSection, kLegacyLayoutTimelineSplitter, 75.0) /
|
|
||||||
100.0;
|
|
||||||
int side = ui::BOTTOM;
|
|
||||||
|
|
||||||
m_customizableDock->undock(m_timeline.get());
|
m_timeline->setVisible(isDoc && (m_mode == NormalMode || m_mode == ContextBarAndTimelineMode) &&
|
||||||
|
pref.general.visibleTimeline());
|
||||||
int w, h;
|
|
||||||
w = h = 64;
|
|
||||||
|
|
||||||
switch (timelinePosition) {
|
|
||||||
case gen::TimelinePosition::LEFT:
|
|
||||||
side = ui::LEFT;
|
|
||||||
w = (workspaceBounds.w * (1.0 - timelineSplitterPos)) / guiscale();
|
|
||||||
break;
|
|
||||||
case gen::TimelinePosition::RIGHT:
|
|
||||||
side = ui::RIGHT;
|
|
||||||
w = (workspaceBounds.w * (1.0 - timelineSplitterPos)) / guiscale();
|
|
||||||
break;
|
|
||||||
case gen::TimelinePosition::BOTTOM:
|
|
||||||
side = ui::BOTTOM;
|
|
||||||
h = (workspaceBounds.h * (1.0 - timelineSplitterPos)) / guiscale();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Listen to resizing changes in the dock that contains the
|
|
||||||
// timeline (so we save the new splitter position)
|
|
||||||
m_timelineResizeConn = m_customizableDock->center()->center()->Resize.connect(
|
|
||||||
[this] { saveTimelineConfiguration(); });
|
|
||||||
|
|
||||||
m_customizableDock->center()->center()->dock(side,
|
|
||||||
m_timeline.get(),
|
|
||||||
gfx::Size(w * guiscale(), h * guiscale()));
|
|
||||||
|
|
||||||
m_timeline->setVisible(isDoc && (m_mode == NormalMode || m_mode == ContextBarAndTimelineMode) &&
|
|
||||||
pref.general.visibleTimeline());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_contextBar->isVisible()) {
|
if (m_contextBar->isVisible()) {
|
||||||
m_contextBar->updateForActiveTool();
|
m_contextBar->updateForActiveTool();
|
||||||
|
|
|
@ -95,7 +95,7 @@ public:
|
||||||
void setDefaultLayout();
|
void setDefaultLayout();
|
||||||
void setMirroredDefaultLayout();
|
void setMirroredDefaultLayout();
|
||||||
void loadUserLayout(const Layout* layout);
|
void loadUserLayout(const Layout* layout);
|
||||||
const Dock* customizableDock() const { return m_customizableDock; }
|
Dock* customizableDock() { return m_customizableDock; }
|
||||||
void setCustomizeDock(bool enable);
|
void setCustomizeDock(bool enable);
|
||||||
|
|
||||||
// When crash::DataRecovery finish to search for sessions, this
|
// When crash::DataRecovery finish to search for sessions, this
|
||||||
|
|
|
@ -73,8 +73,9 @@ Tabs::~Tabs()
|
||||||
m_addedTab.reset();
|
m_addedTab.reset();
|
||||||
m_removedTab.reset();
|
m_removedTab.reset();
|
||||||
|
|
||||||
// Stop animation
|
// Stop animation, can cause issues with docks when stopping during close.
|
||||||
stopAnimation();
|
if (!is_app_state_closing())
|
||||||
|
stopAnimation();
|
||||||
|
|
||||||
// Remove all tabs
|
// Remove all tabs
|
||||||
m_list.clear();
|
m_list.clear();
|
||||||
|
|
|
@ -20,6 +20,10 @@ public:
|
||||||
WorkspaceTabs(TabsDelegate* tabsDelegate);
|
WorkspaceTabs(TabsDelegate* tabsDelegate);
|
||||||
~WorkspaceTabs();
|
~WorkspaceTabs();
|
||||||
|
|
||||||
|
// Dockable impl
|
||||||
|
int dockableAt() const override { return ui::TOP | ui::BOTTOM; }
|
||||||
|
int dockHandleSide() const override { return ui::LEFT; }
|
||||||
|
|
||||||
WorkspacePanel* panel() const { return m_panel; }
|
WorkspacePanel* panel() const { return m_panel; }
|
||||||
void setPanel(WorkspacePanel* panel);
|
void setPanel(WorkspacePanel* panel);
|
||||||
|
|
||||||
|
|
|
@ -38,6 +38,7 @@ public:
|
||||||
|
|
||||||
Items::iterator begin() { return m_items.begin(); }
|
Items::iterator begin() { return m_items.begin(); }
|
||||||
Items::iterator end() { return m_items.end(); }
|
Items::iterator end() { return m_items.end(); }
|
||||||
|
bool empty() const { return m_items.empty(); }
|
||||||
|
|
||||||
void setEditable(bool state);
|
void setEditable(bool state);
|
||||||
void setClickOpen(bool state);
|
void setClickOpen(bool state);
|
||||||
|
|
Loading…
Reference in New Issue