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:
Christian Kaiser 2025-01-30 10:19:22 -03:00 committed by David Capello
parent f556052fc6
commit c445075a79
19 changed files with 856 additions and 492 deletions

View File

@ -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...

View File

@ -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>

View File

@ -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">

View File

@ -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());

View File

@ -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();

View File

@ -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)) {

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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;

View File

@ -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;
} }

View File

@ -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;

View File

@ -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);
} }

View File

@ -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>;

View File

@ -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();

View File

@ -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

View File

@ -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();

View File

@ -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);

View File

@ -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);