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
d9415fdf1b
commit
88e3c2a48c
|
@ -1245,8 +1245,19 @@ default_new_layer_name = New Layer
|
|||
|
||||
[new_layout]
|
||||
title = New Workspace Layout
|
||||
base = Base:
|
||||
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]
|
||||
more = More...
|
||||
|
|
|
@ -3,15 +3,21 @@
|
|||
<gui>
|
||||
<window id="new_layout" text="@.title">
|
||||
<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" />
|
||||
<vbox id="name_placeholder" expansive="true" />
|
||||
</hbox>
|
||||
<entry maxsize="128" id="name" magnet="true" expansive="true" />
|
||||
</grid>
|
||||
|
||||
<separator horizontal="true" />
|
||||
|
||||
<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" />
|
||||
</hbox>
|
||||
</vbox>
|
||||
|
|
|
@ -3,24 +3,12 @@
|
|||
<!-- Copyright (C) 2014-2018 by David Capello -->
|
||||
<gui>
|
||||
<vbox id="timeline_conf">
|
||||
<hbox>
|
||||
<vbox>
|
||||
<separator cell_hspan="2" text="@.position" left="true" horizontal="true" />
|
||||
<hbox>
|
||||
<buttonset columns="2" id="position">
|
||||
<item text="@.left" />
|
||||
<item text="@.right" />
|
||||
<item text="@.bottom" hspan="2" />
|
||||
</buttonset>
|
||||
</hbox>
|
||||
</vbox>
|
||||
<vbox>
|
||||
<separator text="@.frame_header" left="true" horizontal="true" />
|
||||
<hbox>
|
||||
<label text="@.first_frame" />
|
||||
<expr id="first_frame" />
|
||||
</hbox>
|
||||
|
||||
<hbox>
|
||||
<check id="thumb_enabled" text="@.thumbnails" horizontal="true" />
|
||||
<separator id="thumb_h_separator" horizontal="true" expansive="true" />
|
||||
|
@ -34,7 +22,6 @@
|
|||
<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" />
|
||||
<grid columns="2">
|
||||
|
|
|
@ -53,8 +53,6 @@ ConfigureTimelinePopup::ConfigureTimelinePopup()
|
|||
m_box = new app::gen::TimelineConf();
|
||||
addChild(m_box);
|
||||
|
||||
m_box->position()->ItemChange.connect(
|
||||
[this] { onChangeTimelinePosition(m_box->position()->selectedItem()); });
|
||||
m_box->firstFrame()->Change.connect([this] { onChangeFirstFrame(); });
|
||||
m_box->merge()->Click.connect([this] { onChangeType(); });
|
||||
m_box->tint()->Click.connect([this] { onChangeType(); });
|
||||
|
@ -94,15 +92,6 @@ void ConfigureTimelinePopup::updateWidgetsFromCurrentSettings()
|
|||
DocumentPreferences& docPref = this->docPref();
|
||||
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());
|
||||
|
||||
switch (docPref.onionskin.type()) {
|
||||
|
@ -148,19 +137,6 @@ bool ConfigureTimelinePopup::onProcessMessage(ui::Message* 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()
|
||||
{
|
||||
docPref().timeline.firstFrame(m_box->firstFrame()->textInt());
|
||||
|
|
|
@ -31,8 +31,6 @@ class ConfigureTimelinePopup : public ui::PopupWindow {
|
|||
public:
|
||||
ConfigureTimelinePopup();
|
||||
|
||||
static void onChangeTimelinePosition(int option);
|
||||
|
||||
protected:
|
||||
bool onProcessMessage(ui::Message* msg) override;
|
||||
void onChangeFirstFrame();
|
||||
|
|
|
@ -10,9 +10,19 @@
|
|||
|
||||
#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/layout_selector.h"
|
||||
#include "app/ui/main_window.h"
|
||||
#include "app/ui/skin/skin_theme.h"
|
||||
#include "os/system.h"
|
||||
#include "ui/cursor_type.h"
|
||||
#include "ui/label.h"
|
||||
#include "ui/menu.h"
|
||||
#include "ui/message.h"
|
||||
#include "ui/paint_event.h"
|
||||
#include "ui/resize_event.h"
|
||||
|
@ -54,34 +64,86 @@ int side_from_index(int index)
|
|||
|
||||
} // 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;
|
||||
for (auto child : children()) {
|
||||
if (child->isVisible())
|
||||
sz |= child->sizeHint();
|
||||
setExpansive(true);
|
||||
setSizeHint(dragWidget->sizeHint());
|
||||
setMinSize(dragWidget->size());
|
||||
|
||||
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();
|
||||
setBoundsQuietly(bounds);
|
||||
bounds = childrenBounds();
|
||||
bounds.y += textHeight();
|
||||
bounds.h -= textHeight();
|
||||
|
||||
for (auto child : children()) {
|
||||
child->setBounds(bounds);
|
||||
}
|
||||
display()->removeLayer(m_floatingUILayer);
|
||||
}
|
||||
|
||||
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();
|
||||
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()
|
||||
|
@ -104,10 +166,11 @@ void Dock::setCustomizing(bool enable, bool doLayout)
|
|||
m_customizing = enable;
|
||||
|
||||
for (int i = 0; i < kSides; ++i) {
|
||||
auto child = m_sides[i];
|
||||
auto* child = m_sides[i];
|
||||
if (!child)
|
||||
continue;
|
||||
else if (auto subdock = dynamic_cast<Dock*>(child))
|
||||
|
||||
if (auto* subdock = dynamic_cast<Dock*>(child))
|
||||
subdock->setCustomizing(enable, false);
|
||||
}
|
||||
|
||||
|
@ -118,23 +181,16 @@ void Dock::setCustomizing(bool enable, bool doLayout)
|
|||
void Dock::resetDocks()
|
||||
{
|
||||
for (int i = 0; i < kSides; ++i) {
|
||||
auto child = m_sides[i];
|
||||
auto* child = m_sides[i];
|
||||
if (!child)
|
||||
continue;
|
||||
else if (auto subdock = dynamic_cast<Dock*>(child)) {
|
||||
|
||||
if (auto* subdock = dynamic_cast<Dock*>(child)) {
|
||||
subdock->resetDocks();
|
||||
if (subdock->m_autoDelete)
|
||||
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;
|
||||
}
|
||||
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])) {
|
||||
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 {
|
||||
auto oldWidget = m_sides[i];
|
||||
auto newTabs = new DockTabs;
|
||||
replaceChild(oldWidget, newTabs);
|
||||
newTabs->addChild(oldWidget);
|
||||
newTabs->addChild(widget);
|
||||
setSide(i, newTabs);
|
||||
ASSERT(false); // Docking failure!
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -180,7 +226,7 @@ void Dock::dockRelativeTo(ui::Widget* relative,
|
|||
Widget* parent = relative->parent();
|
||||
ASSERT(parent);
|
||||
|
||||
Dock* subdock = new Dock;
|
||||
auto* subdock = new Dock;
|
||||
subdock->m_autoDelete = true;
|
||||
subdock->m_customizing = m_customizing;
|
||||
parent->replaceChild(relative, subdock);
|
||||
|
@ -188,7 +234,7 @@ void Dock::dockRelativeTo(ui::Widget* relative,
|
|||
subdock->dock(side, widget, prefSize);
|
||||
|
||||
// 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) {
|
||||
if (relativeDock->m_sides[i] == relative) {
|
||||
relativeDock->setSide(i, subdock);
|
||||
|
@ -204,12 +250,13 @@ void Dock::undock(Widget* widget)
|
|||
if (!parent)
|
||||
return; // Already undocked
|
||||
|
||||
if (auto parentDock = dynamic_cast<Dock*>(parent)) {
|
||||
if (auto* parentDock = dynamic_cast<Dock*>(parent)) {
|
||||
parentDock->removeChild(widget);
|
||||
|
||||
for (int i = 0; i < kSides; ++i) {
|
||||
if (parentDock->m_sides[i] == widget) {
|
||||
parentDock->setSide(i, nullptr);
|
||||
m_sizes[i] = gfx::Size();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -218,13 +265,6 @@ void Dock::undock(Widget* widget)
|
|||
undock(parentDock);
|
||||
}
|
||||
}
|
||||
else if (auto parentTabs = dynamic_cast<DockTabs*>(parent)) {
|
||||
parentTabs->removeChild(widget);
|
||||
|
||||
if (parentTabs->children().empty()) {
|
||||
undock(parentTabs);
|
||||
}
|
||||
}
|
||||
else {
|
||||
parent->removeChild(widget);
|
||||
}
|
||||
|
@ -238,25 +278,25 @@ int Dock::whichSideChildIsDocked(const ui::Widget* widget) const
|
|||
return 0;
|
||||
}
|
||||
|
||||
gfx::Size Dock::getUserDefinedSizeAtSide(int side) const
|
||||
const gfx::Size Dock::getUserDefinedSizeAtSide(int side) const
|
||||
{
|
||||
int i = side_index(side);
|
||||
// Only EXPANSIVE sides can be user-defined (has a splitter so the
|
||||
// user can expand or shrink it)
|
||||
if (m_aligns[i] & EXPANSIVE)
|
||||
return m_sizes[i];
|
||||
else
|
||||
|
||||
return gfx::Size();
|
||||
}
|
||||
|
||||
Dock* Dock::subdock(int 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;
|
||||
|
||||
auto oldWidget = m_sides[i];
|
||||
auto newSubdock = new Dock;
|
||||
auto* oldWidget = m_sides[i];
|
||||
auto* newSubdock = new Dock;
|
||||
newSubdock->m_autoDelete = true;
|
||||
newSubdock->m_customizing = m_customizing;
|
||||
setSide(i, newSubdock);
|
||||
|
@ -291,7 +331,7 @@ void Dock::onSizeHint(ui::SizeHintEvent& ev)
|
|||
|
||||
void Dock::onResize(ui::ResizeEvent& ev)
|
||||
{
|
||||
auto bounds = ev.bounds();
|
||||
gfx::Rect bounds = ev.bounds();
|
||||
setBoundsQuietly(bounds);
|
||||
bounds = childrenBounds();
|
||||
|
||||
|
@ -302,11 +342,11 @@ void Dock::onResize(ui::ResizeEvent& ev)
|
|||
const gfx::Rect& widgetBounds,
|
||||
const gfx::Rect& separator,
|
||||
const int index) {
|
||||
auto rc = widgetBounds;
|
||||
gfx::Rect rc = widgetBounds;
|
||||
auto th = textHeight();
|
||||
if (isCustomizing()) {
|
||||
int handleSide = 0;
|
||||
if (auto dockable = dynamic_cast<Dockable*>(widget))
|
||||
if (auto* dockable = dynamic_cast<Dockable*>(widget))
|
||||
handleSide = dockable->dockHandleSide();
|
||||
switch (handleSide) {
|
||||
case ui::TOP:
|
||||
|
@ -326,7 +366,8 @@ void Dock::onResize(ui::ResizeEvent& ev)
|
|||
void Dock::onPaint(ui::PaintEvent& ev)
|
||||
{
|
||||
Graphics* g = ev.graphics();
|
||||
gfx::Rect bounds = clientBounds();
|
||||
|
||||
const gfx::Rect& bounds = clientBounds();
|
||||
g->fillRect(bgColor(), bounds);
|
||||
|
||||
if (isCustomizing()) {
|
||||
|
@ -335,13 +376,13 @@ void Dock::onPaint(ui::PaintEvent& ev)
|
|||
const gfx::Rect& widgetBounds,
|
||||
const gfx::Rect& separator,
|
||||
const int index) {
|
||||
auto rc = widgetBounds;
|
||||
gfx::Rect rc = widgetBounds;
|
||||
auto th = textHeight();
|
||||
if (isCustomizing()) {
|
||||
auto theme = SkinTheme::get(this);
|
||||
auto color = theme->colors.workspaceText();
|
||||
auto* theme = SkinTheme::get(this);
|
||||
const auto& color = theme->colors.workspaceText();
|
||||
int handleSide = 0;
|
||||
if (auto dockable = dynamic_cast<Dockable*>(widget))
|
||||
if (auto* dockable = dynamic_cast<Dockable*>(widget))
|
||||
handleSide = dockable->dockHandleSide();
|
||||
switch (handleSide) {
|
||||
case ui::TOP:
|
||||
|
@ -377,19 +418,20 @@ bool Dock::onProcessMessage(ui::Message* msg)
|
|||
{
|
||||
switch (msg->type()) {
|
||||
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) {
|
||||
m_startPos = pos;
|
||||
|
||||
if (m_hit.sideIndex >= 0) {
|
||||
if (m_hit.sideIndex >= 0)
|
||||
m_startSize = m_sizes[m_hit.sideIndex];
|
||||
}
|
||||
|
||||
captureMouse();
|
||||
|
||||
if (m_hit.dockable)
|
||||
invalidate();
|
||||
if (m_hit.dockable && !mouseMessage->right()) {
|
||||
m_dragging = true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
@ -398,22 +440,98 @@ bool Dock::onProcessMessage(ui::Message* msg)
|
|||
|
||||
case kMouseMoveMessage: {
|
||||
if (hasCapture()) {
|
||||
const gfx::Point& pos = static_cast<MouseMessage*>(msg)->position();
|
||||
|
||||
if (m_dropzonePlaceholder)
|
||||
m_dropzonePlaceholder->setGhostPosition(pos);
|
||||
|
||||
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 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) {
|
||||
case kTopIndex: sz.h = (m_startSize.h + pos.y - m_startPos.y); break;
|
||||
case kBottomIndex: sz.h = (m_startSize.h - pos.y + m_startPos.y); break;
|
||||
case kLeftIndex: sz.w = (m_startSize.w + pos.x - m_startPos.x); break;
|
||||
case kRightIndex: sz.w = (m_startSize.w - pos.x + m_startPos.x); break;
|
||||
case kTopIndex: sz.h = std::max(m_startSize.h + pos.y - m_startPos.y, minSize.h); break;
|
||||
case kBottomIndex:
|
||||
sz.h = std::max(m_startSize.h - pos.y + m_startPos.y, minSize.h);
|
||||
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();
|
||||
Resize();
|
||||
}
|
||||
else if (m_hit.dockable) {
|
||||
else if (m_hit.dockable && m_dragging) {
|
||||
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;
|
||||
|
@ -422,13 +540,76 @@ bool Dock::onProcessMessage(ui::Message* msg)
|
|||
case kMouseUpMessage: {
|
||||
if (hasCapture()) {
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
if (!hasCapture())
|
||||
|
@ -442,59 +623,10 @@ bool Dock::onProcessMessage(ui::Message* msg)
|
|||
case kRightIndex: cursor = ui::kSizeWECursor; break;
|
||||
}
|
||||
}
|
||||
else if (m_hit.dockable) {
|
||||
else if (m_hit.dockable && m_hit.targetSide == -1) {
|
||||
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);
|
||||
return true;
|
||||
}
|
||||
|
@ -511,7 +643,7 @@ void Dock::onUserResizedDock()
|
|||
|
||||
// Send the same notification for the parent (as probably eh
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
|
@ -533,19 +665,10 @@ int Dock::calcAlign(const int i)
|
|||
if (!widget) {
|
||||
// Do nothing
|
||||
}
|
||||
else if (auto subdock = dynamic_cast<Dock*>(widget)) {
|
||||
else if (auto* subdock = dynamic_cast<Dock*>(widget)) {
|
||||
align = subdock->calcAlign(i);
|
||||
}
|
||||
else if (auto tabs = dynamic_cast<DockTabs*>(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)) {
|
||||
else if (auto* dockable2 = dynamic_cast<Dockable*>(widget)) {
|
||||
align = dockable2->dockableAt();
|
||||
}
|
||||
return align;
|
||||
|
@ -560,28 +683,15 @@ void Dock::updateDockVisibility()
|
|||
if (!widget)
|
||||
continue;
|
||||
|
||||
if (auto subdock = dynamic_cast<Dock*>(widget)) {
|
||||
if (auto* subdock = dynamic_cast<Dock*>(widget)) {
|
||||
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()) {
|
||||
visible = true;
|
||||
}
|
||||
}
|
||||
|
||||
setVisible(visible);
|
||||
}
|
||||
|
||||
|
@ -592,8 +702,8 @@ void Dock::forEachSide(gfx::Rect bounds,
|
|||
const int index)> f)
|
||||
{
|
||||
for (int i = 0; i < kSides; ++i) {
|
||||
auto widget = m_sides[i];
|
||||
if (!widget || !widget->isVisible()) {
|
||||
auto* widget = m_sides[i];
|
||||
if (!widget || !widget->isVisible() || widget->isDecorative()) {
|
||||
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)
|
||||
{
|
||||
Hit hit;
|
||||
|
@ -664,11 +806,9 @@ Dock::Hit Dock::calcHit(const gfx::Point& pos)
|
|||
}
|
||||
else if (isCustomizing()) {
|
||||
auto th = textHeight();
|
||||
auto rc = widgetBounds;
|
||||
auto theme = SkinTheme::get(this);
|
||||
if (auto dockable = dynamic_cast<Dockable*>(widget)) {
|
||||
int handleSide = dockable->dockHandleSide();
|
||||
switch (handleSide) {
|
||||
gfx::Rect rc = widgetBounds;
|
||||
if (auto* dockable = dynamic_cast<Dockable*>(widget)) {
|
||||
switch (dockable->dockHandleSide()) {
|
||||
case ui::TOP:
|
||||
rc.h = th;
|
||||
if (rc.contains(pos)) {
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
#define APP_UI_DOCK_H_INCLUDED
|
||||
#pragma once
|
||||
|
||||
#include "app/ui/dockable.h"
|
||||
#include "gfx/rect.h"
|
||||
#include "gfx/size.h"
|
||||
#include "ui/widget.h"
|
||||
|
@ -21,18 +22,26 @@ namespace app {
|
|||
|
||||
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 {
|
||||
public:
|
||||
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();
|
||||
|
||||
bool isCustomizing() const { return m_customizing; }
|
||||
|
@ -60,7 +69,7 @@ public:
|
|||
|
||||
// Functions useful to query/save the dock layout.
|
||||
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()> UserResizedDock;
|
||||
|
@ -74,8 +83,8 @@ protected:
|
|||
void onUserResizedDock();
|
||||
|
||||
private:
|
||||
void setSide(const int i, ui::Widget* newWidget);
|
||||
int calcAlign(const int i);
|
||||
void setSide(int i, ui::Widget* newWidget);
|
||||
int calcAlign(int i);
|
||||
void updateDockVisibility();
|
||||
void forEachSide(gfx::Rect bounds,
|
||||
std::function<void(ui::Widget* widget,
|
||||
|
@ -84,11 +93,13 @@ private:
|
|||
const int index)> f);
|
||||
|
||||
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 {
|
||||
ui::Widget* widget = nullptr;
|
||||
Dockable* dockable = nullptr;
|
||||
int sideIndex = -1;
|
||||
int targetSide = -1;
|
||||
};
|
||||
|
||||
Hit calcHit(const gfx::Point& pos);
|
||||
|
@ -107,6 +118,10 @@ private:
|
|||
|
||||
// True when we paint/can drag-and-drop dockable widgets from handles.
|
||||
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
|
||||
|
|
|
@ -36,10 +36,10 @@ void IconButton::setIcon(const skin::SkinPartPtr& part)
|
|||
|
||||
void IconButton::onInitTheme(InitThemeEvent& ev)
|
||||
{
|
||||
Button::onInitTheme(ev);
|
||||
|
||||
auto theme = SkinTheme::get(this);
|
||||
setBgColor(theme->colors.menuitemNormalFace());
|
||||
|
||||
Button::onInitTheme(ev);
|
||||
}
|
||||
|
||||
void IconButton::onSizeHint(SizeHintEvent& ev)
|
||||
|
|
|
@ -33,9 +33,9 @@ using namespace tinyxml2;
|
|||
|
||||
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 gfx::Size size = dock->getUserDefinedSizeAtSide(side);
|
||||
const gfx::Size& size = dock->getUserDefinedSizeAtSide(side);
|
||||
|
||||
std::string sideStr;
|
||||
switch (side) {
|
||||
|
@ -44,13 +44,14 @@ static void save_dock_layout(XMLElement* elem, const Dock* dock)
|
|||
case ui::TOP: sideStr = "top"; break;
|
||||
case ui::BOTTOM: sideStr = "bottom"; break;
|
||||
case ui::CENTER:
|
||||
default:
|
||||
// Empty side attribute
|
||||
break;
|
||||
}
|
||||
|
||||
XMLElement* childElem = elem->InsertNewChildElement("");
|
||||
|
||||
if (auto subdock = dynamic_cast<const Dock*>(child)) {
|
||||
if (const auto* subdock = dynamic_cast<const Dock*>(child)) {
|
||||
childElem->SetValue("dock");
|
||||
if (!sideStr.empty())
|
||||
childElem->SetAttribute("side", sideStr.c_str());
|
||||
|
@ -87,7 +88,7 @@ static void load_dock_layout(const XMLElement* elem, Dock* dock)
|
|||
Dock* subdock = nullptr;
|
||||
|
||||
int side = ui::CENTER;
|
||||
if (auto sideStr = elem->Attribute("side")) {
|
||||
if (const auto* sideStr = elem->Attribute("side")) {
|
||||
if (std::strcmp(sideStr, "left") == 0)
|
||||
side = ui::LEFT;
|
||||
if (std::strcmp(sideStr, "right") == 0)
|
||||
|
@ -129,7 +130,7 @@ static void load_dock_layout(const XMLElement* elem, Dock* dock)
|
|||
}
|
||||
|
||||
if (subdock) {
|
||||
auto childElem = elem->FirstChildElement();
|
||||
const auto* childElem = elem->FirstChildElement();
|
||||
while (childElem) {
|
||||
load_dock_layout(childElem, subdock);
|
||||
childElem = childElem->NextSiblingElement();
|
||||
|
@ -143,12 +144,27 @@ static void load_dock_layout(const XMLElement* elem, Dock* dock)
|
|||
// static
|
||||
LayoutPtr Layout::MakeFromXmlElement(const XMLElement* layoutElem)
|
||||
{
|
||||
auto layout = std::make_shared<Layout>();
|
||||
if (auto name = layoutElem->Attribute("name")) {
|
||||
layout->m_id = name;
|
||||
layout->m_name = name;
|
||||
const char* name = layoutElem->Attribute("name");
|
||||
const char* id = layoutElem->Attribute("id");
|
||||
|
||||
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();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -160,19 +176,21 @@ LayoutPtr Layout::MakeFromDock(const std::string& id, const std::string& name, c
|
|||
layout->m_name = name;
|
||||
|
||||
layout->m_elem = layout->m_dummyDoc.NewElement("layout");
|
||||
layout->m_elem->SetAttribute("id", id.c_str());
|
||||
layout->m_elem->SetAttribute("name", name.c_str());
|
||||
save_dock_layout(layout->m_elem, dock);
|
||||
|
||||
return layout;
|
||||
}
|
||||
|
||||
bool Layout::matchId(const std::string& id) const
|
||||
bool Layout::matchId(const std::string_view id) const
|
||||
{
|
||||
if (m_id == id)
|
||||
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;
|
||||
else
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -190,4 +208,15 @@ bool Layout::loadLayout(Dock* dock) const
|
|||
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
|
||||
|
|
|
@ -24,6 +24,9 @@ public:
|
|||
static constexpr const char* kDefault = "_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 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 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 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:
|
||||
std::string m_id;
|
||||
std::string m_name;
|
||||
|
|
|
@ -13,12 +13,15 @@
|
|||
#include "app/app.h"
|
||||
#include "app/i18n/strings.h"
|
||||
#include "app/match_words.h"
|
||||
#include "app/ui/button_set.h"
|
||||
#include "app/ui/configure_timeline_popup.h"
|
||||
#include "app/pref/preferences.h"
|
||||
#include "app/ui/main_window.h"
|
||||
#include "app/ui/separator_in_view.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/label.h"
|
||||
#include "ui/listitem.h"
|
||||
#include "ui/tooltips.h"
|
||||
#include "ui/window.h"
|
||||
|
@ -34,57 +37,11 @@ using namespace ui;
|
|||
|
||||
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
|
||||
// and GotoFrameCommand::TagsEntry
|
||||
class LayoutsEntry : public ComboBox {
|
||||
class LayoutsEntry final : public ComboBox {
|
||||
public:
|
||||
LayoutsEntry(Layouts& layouts) : m_layouts(layouts)
|
||||
explicit LayoutsEntry(Layouts& layouts) : m_layouts(layouts)
|
||||
{
|
||||
setEditable(true);
|
||||
getEntryWidget()->Change.connect(&LayoutsEntry::onEntryChange, this);
|
||||
|
@ -96,26 +53,32 @@ private:
|
|||
{
|
||||
deleteAllItems();
|
||||
|
||||
MatchWords match(getEntryWidget()->text());
|
||||
const MatchWords match(getEntryWidget()->text());
|
||||
|
||||
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())) {
|
||||
matchAny = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (auto& layout : m_layouts) {
|
||||
for (const auto& layout : m_layouts) {
|
||||
if (layout->isDefault())
|
||||
continue;
|
||||
|
||||
if (all || !matchAny || match(layout->name()))
|
||||
addItem(layout->name());
|
||||
}
|
||||
}
|
||||
|
||||
void onEntryChange()
|
||||
void onEntryChange() override
|
||||
{
|
||||
closeListBox();
|
||||
fill(false);
|
||||
if (getItemCount() > 0)
|
||||
if (getItemCount() > 0 && !empty())
|
||||
openListBox();
|
||||
}
|
||||
|
||||
|
@ -124,9 +87,9 @@ private:
|
|||
|
||||
}; // namespace
|
||||
|
||||
class LayoutSelector::LayoutItem : public ListItem {
|
||||
class LayoutSelector::LayoutItem final : public ListItem {
|
||||
public:
|
||||
enum LayoutOption {
|
||||
enum LayoutOption : uint8_t {
|
||||
DEFAULT,
|
||||
MIRRORED_DEFAULT,
|
||||
USER_DEFINED,
|
||||
|
@ -136,88 +99,210 @@ public:
|
|||
LayoutItem(LayoutSelector* selector,
|
||||
const LayoutOption option,
|
||||
const std::string& text,
|
||||
const LayoutPtr layout)
|
||||
const std::string& layoutId = "")
|
||||
: ListItem(text)
|
||||
, m_option(option)
|
||||
, m_selector(selector)
|
||||
, m_layout(layout)
|
||||
, m_layoutId(layoutId)
|
||||
{
|
||||
auto* hbox = new HBox;
|
||||
hbox->setTransparent(true);
|
||||
addChild(hbox);
|
||||
|
||||
auto* filler = new BoxFiller();
|
||||
filler->setTransparent(true);
|
||||
hbox->addChild(filler);
|
||||
|
||||
if (option == USER_DEFINED ||
|
||||
((option == DEFAULT || option == MIRRORED_DEFAULT) && !layoutId.empty())) {
|
||||
addActionButton();
|
||||
}
|
||||
}
|
||||
|
||||
std::string getLayoutId() const
|
||||
// Separated from the constructor so we can add it on the fly when modifying Default/Mirrored
|
||||
void addActionButton(const std::string& newLayoutId = "")
|
||||
{
|
||||
if (m_layout)
|
||||
return m_layout->id();
|
||||
else
|
||||
return std::string();
|
||||
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();
|
||||
}
|
||||
|
||||
bool matchId(const std::string& id) const { return (m_layout && m_layout->matchId(id)); }
|
||||
|
||||
const LayoutPtr& layout() const { return m_layout; }
|
||||
|
||||
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;
|
||||
m_selector->removeLayout(m_layoutId);
|
||||
}
|
||||
// Even Default & Mirrored Default can have a customized layout
|
||||
// (customized default layout).
|
||||
if (m_layout)
|
||||
win->loadUserLayout(m_layout.get());
|
||||
});
|
||||
}
|
||||
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();
|
||||
}
|
||||
|
||||
void selectAfterClose()
|
||||
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();
|
||||
|
||||
switch (m_option) {
|
||||
case LayoutOption::NEW_LAYOUT: {
|
||||
// Select the "Layout" separator (it's like selecting nothing)
|
||||
// TODO improve the ComboBox to select a real "nothing" (with
|
||||
// a placeholder text)
|
||||
m_selector->m_comboBox.setSelectedItemIndex(0);
|
||||
case DEFAULT: {
|
||||
if (const auto& defaultLayout = win->layoutSelector()->m_layouts.getById(
|
||||
Layout::kDefault)) {
|
||||
win->loadUserLayout(defaultLayout.get());
|
||||
}
|
||||
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;
|
||||
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);
|
||||
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()));
|
||||
}
|
||||
else {
|
||||
item = new ListItem(layout->name());
|
||||
}
|
||||
|
||||
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()) {
|
||||
auto layout =
|
||||
Layout::MakeFromDock(name.getValue(), name.getValue(), win->customizableDock());
|
||||
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());
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
// Do nothing
|
||||
break;
|
||||
else {
|
||||
// Ensure we go back to having the layout we were at selected.
|
||||
m_selector->populateComboBox();
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
LayoutOption m_option;
|
||||
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()
|
||||
{
|
||||
ComboBox::onChange();
|
||||
if (auto item = dynamic_cast<LayoutItem*>(getSelectedItem())) {
|
||||
if (auto* item = dynamic_cast<LayoutItem*>(getSelectedItem())) {
|
||||
item->selectImmediately();
|
||||
m_selected = item;
|
||||
}
|
||||
|
@ -235,7 +320,7 @@ void LayoutSelector::LayoutComboBox::onCloseListBox()
|
|||
LayoutSelector::LayoutSelector(TooltipManager* tooltipManager)
|
||||
: m_button(SkinTheme::instance()->parts.iconUserData())
|
||||
{
|
||||
m_activeLayoutId = Preferences::instance().general.workspaceLayout();
|
||||
setActiveLayoutId(Preferences::instance().general.workspaceLayout());
|
||||
|
||||
m_button.Click.connect([this]() { switchSelector(); });
|
||||
|
||||
|
@ -258,44 +343,55 @@ LayoutSelector::~LayoutSelector()
|
|||
{
|
||||
Preferences::instance().general.workspaceLayout(m_activeLayoutId);
|
||||
|
||||
if (!is_app_state_closing())
|
||||
stopAnimation();
|
||||
}
|
||||
|
||||
LayoutPtr LayoutSelector::activeLayout()
|
||||
LayoutPtr LayoutSelector::activeLayout() const
|
||||
{
|
||||
return m_layouts.getById(m_activeLayoutId);
|
||||
}
|
||||
|
||||
void LayoutSelector::addLayout(const LayoutPtr& layout)
|
||||
{
|
||||
bool added = 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_layouts.addLayout(layout);
|
||||
|
||||
m_comboBox.setSelectedItem(item);
|
||||
}
|
||||
else {
|
||||
for (auto item : m_comboBox) {
|
||||
if (auto layoutItem = dynamic_cast<LayoutItem*>(item)) {
|
||||
if (layoutItem->layout() && layoutItem->layout()->name() == layout->name()) {
|
||||
layoutItem->setLayout(layout);
|
||||
m_comboBox.setSelectedItem(item);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// HACK: Because this function is called from inside a LayoutItem, clearing the combobox items
|
||||
// will crash.
|
||||
// TODO: Is there a better way to do this?
|
||||
auto* msg = new CallbackMessage([this] { populateComboBox(); });
|
||||
msg->setRecipient(this);
|
||||
manager()->enqueueMessage(msg);
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
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
|
||||
// update a layout it must be existent in the m_layouts collection.
|
||||
ASSERT(result == false);
|
||||
if (added && newLayout->isDefault()) {
|
||||
// Mark it with an asterisk if we're editing a default layout.
|
||||
populateComboBox();
|
||||
}
|
||||
}
|
||||
|
||||
void LayoutSelector::onAnimationFrame()
|
||||
|
@ -311,7 +407,7 @@ void LayoutSelector::onAnimationFrame()
|
|||
}
|
||||
}
|
||||
|
||||
if (auto win = window())
|
||||
if (auto* win = window())
|
||||
win->layout();
|
||||
}
|
||||
|
||||
|
@ -335,7 +431,7 @@ void LayoutSelector::onAnimationStop(int animation)
|
|||
break;
|
||||
}
|
||||
|
||||
if (auto win = window())
|
||||
if (auto* win = window())
|
||||
win->layout();
|
||||
}
|
||||
|
||||
|
@ -347,23 +443,7 @@ void LayoutSelector::switchSelector()
|
|||
|
||||
// Create the combobox for first time
|
||||
if (m_comboBox.getItemCount() == 0) {
|
||||
m_comboBox.addItem(new SeparatorInView(Strings::main_window_layout(), HORIZONTAL));
|
||||
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));
|
||||
populateComboBox();
|
||||
}
|
||||
|
||||
m_comboBox.setVisible(true);
|
||||
|
@ -377,7 +457,7 @@ void LayoutSelector::switchSelector()
|
|||
m_endSize = gfx::Size(0, 0);
|
||||
}
|
||||
|
||||
if (auto item = getItemByLayoutId(m_activeLayoutId))
|
||||
if (auto* item = getItemByLayoutId(m_activeLayoutId))
|
||||
m_comboBox.setSelectedItem(item);
|
||||
|
||||
m_comboBox.setSizeHint(m_startSize);
|
||||
|
@ -403,14 +483,53 @@ void LayoutSelector::setupTooltips(TooltipManager* tooltipManager)
|
|||
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)
|
||||
{
|
||||
for (auto child : m_comboBox) {
|
||||
if (auto item = dynamic_cast<LayoutItem*>(child)) {
|
||||
if (item->matchId(id))
|
||||
for (auto* child : m_comboBox) {
|
||||
if (auto* item = dynamic_cast<LayoutItem*>(child)) {
|
||||
if (item->getLayoutId() == id)
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
|
|
|
@ -47,10 +47,12 @@ public:
|
|||
LayoutSelector(ui::TooltipManager* tooltipManager);
|
||||
~LayoutSelector();
|
||||
|
||||
LayoutPtr activeLayout();
|
||||
std::string activeLayoutId() const { return m_activeLayoutId; }
|
||||
LayoutPtr activeLayout() const;
|
||||
const std::string& activeLayoutId() const { return m_activeLayoutId; }
|
||||
|
||||
void addLayout(const LayoutPtr& layout);
|
||||
void removeLayout(const LayoutPtr& layout);
|
||||
void removeLayout(const std::string& layoutId);
|
||||
void updateActiveLayout(const LayoutPtr& layout);
|
||||
void switchSelector();
|
||||
void switchSelectorFromCommand();
|
||||
|
@ -61,6 +63,20 @@ public:
|
|||
|
||||
private:
|
||||
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);
|
||||
void onAnimationFrame() override;
|
||||
void onAnimationStop(int animation) override;
|
||||
|
|
|
@ -36,8 +36,12 @@ Layouts::Layouts()
|
|||
|
||||
Layouts::~Layouts()
|
||||
{
|
||||
if (!m_userLayoutsFilename.empty())
|
||||
save(m_userLayoutsFilename);
|
||||
try {
|
||||
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
|
||||
|
@ -50,28 +54,69 @@ LayoutPtr Layouts::getById(const std::string& id) const
|
|||
|
||||
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());
|
||||
});
|
||||
|
||||
if (it != m_layouts.end()) {
|
||||
*it = layout; // Replace existent layout
|
||||
return false;
|
||||
}
|
||||
else {
|
||||
|
||||
m_layouts.push_back(layout);
|
||||
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)
|
||||
{
|
||||
XMLDocumentRef doc = app::open_xml(fn);
|
||||
const XMLDocumentRef doc = app::open_xml(fn);
|
||||
XMLHandle handle(doc.get());
|
||||
XMLElement* layoutElem =
|
||||
handle.FirstChildElement("layouts").FirstChildElement("layout").ToElement();
|
||||
|
||||
while (layoutElem) {
|
||||
m_layouts.push_back(Layout::MakeFromXmlElement(layoutElem));
|
||||
if (auto layout = Layout::MakeFromXmlElement(layoutElem)) {
|
||||
m_layouts.push_back(layout);
|
||||
}
|
||||
layoutElem = layoutElem->NextSiblingElement();
|
||||
}
|
||||
}
|
||||
|
@ -85,7 +130,7 @@ void Layouts::save(const std::string& fn) const
|
|||
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);
|
||||
save_xml(doc.get(), fn);
|
||||
}
|
||||
|
|
|
@ -27,6 +27,10 @@ public:
|
|||
// Returns true if the layout is added, or false if it was
|
||||
// replaced.
|
||||
bool addLayout(const LayoutPtr& layout);
|
||||
void removeLayout(const LayoutPtr& layout);
|
||||
|
||||
void saveUserLayouts();
|
||||
void reload();
|
||||
|
||||
// To iterate layouts
|
||||
using List = std::vector<LayoutPtr>;
|
||||
|
|
|
@ -60,9 +60,9 @@ namespace app {
|
|||
|
||||
using namespace ui;
|
||||
|
||||
static const char* kLegacyLayoutMainWindowSection = "layout:main_window";
|
||||
static const char* kLegacyLayoutTimelineSplitter = "timeline_splitter";
|
||||
static const char* kLegacyLayoutColorBarSplitter = "color_bar_splitter";
|
||||
static constexpr const char* kLegacyLayoutMainWindowSection = "layout:main_window";
|
||||
static constexpr const char* kLegacyLayoutTimelineSplitter = "timeline_splitter";
|
||||
static constexpr const char* kLegacyLayoutColorBarSplitter = "color_bar_splitter";
|
||||
|
||||
class ScreenScalePanic : public INotificationDelegate {
|
||||
public:
|
||||
|
@ -185,8 +185,8 @@ void MainWindow::initialize()
|
|||
m_dock->dock(ui::CENTER, m_customizableDockPlaceholder.get());
|
||||
|
||||
// After the user resizes the dock we save the updated layout
|
||||
m_saveDockLayoutConn = m_customizableDock->UserResizedDock.connect(
|
||||
[this] { saveActiveLayout(); });
|
||||
m_saveDockLayoutConn = m_customizableDock->UserResizedDock.connect(&MainWindow::saveActiveLayout,
|
||||
this);
|
||||
|
||||
setDefaultLayout();
|
||||
if (LayoutPtr layout = m_layoutSelector->activeLayout())
|
||||
|
@ -202,7 +202,7 @@ void MainWindow::initialize()
|
|||
|
||||
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.
|
||||
Strings::instance()->LanguageChange.connect([this] { onLanguageChange(); });
|
||||
}
|
||||
|
@ -216,6 +216,10 @@ MainWindow::~MainWindow()
|
|||
m_dock->resetDocks();
|
||||
m_customizableDock->resetDocks();
|
||||
|
||||
// Leaving them in can cause crashes when cleaning up.
|
||||
m_dock = nullptr;
|
||||
m_customizableDock = nullptr;
|
||||
|
||||
m_layoutSelector.reset();
|
||||
m_scalePanic.reset();
|
||||
|
||||
|
@ -386,9 +390,7 @@ void MainWindow::setTimelineVisibility(bool visible)
|
|||
|
||||
void MainWindow::popTimeline()
|
||||
{
|
||||
Preferences& preferences = Preferences::instance();
|
||||
|
||||
if (!preferences.general.autoshowTimeline())
|
||||
if (!Preferences::instance().general.autoshowTimeline())
|
||||
return;
|
||||
|
||||
if (!getTimelineVisibility())
|
||||
|
@ -400,7 +402,7 @@ void MainWindow::setDefaultLayout()
|
|||
m_timelineResizeConn.disconnect();
|
||||
m_colorBarResizeConn.disconnect();
|
||||
|
||||
auto colorBarWidth = get_config_double(kLegacyLayoutMainWindowSection,
|
||||
const auto colorBarWidth = get_config_double(kLegacyLayoutMainWindowSection,
|
||||
kLegacyLayoutColorBarSplitter,
|
||||
m_colorBar->sizeHint().w);
|
||||
|
||||
|
@ -409,9 +411,33 @@ void MainWindow::setDefaultLayout()
|
|||
m_customizableDock->dock(ui::BOTTOM, m_statusBar.get());
|
||||
m_customizableDock->center()->dock(ui::TOP, m_contextBar.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(),
|
||||
gfx::Size(64 * guiscale(), 64 * guiscale()));
|
||||
timelineSize.createUnion(gfx::Size(64, 64)));
|
||||
|
||||
m_customizableDock->center()->center()->dock(ui::CENTER, m_workspace.get());
|
||||
configureWorkspaceLayout();
|
||||
}
|
||||
|
@ -444,8 +470,10 @@ void MainWindow::loadUserLayout(const Layout* layout)
|
|||
|
||||
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();
|
||||
}
|
||||
|
||||
this->layout();
|
||||
}
|
||||
|
@ -510,7 +538,7 @@ void MainWindow::onActiveViewChange()
|
|||
{
|
||||
// If we are closing the app, we just ignore all view changes (as
|
||||
// docs will be destroyed and views closed).
|
||||
if (get_app_state() != AppState::kNormal)
|
||||
if (get_app_state() != AppState::kNormal || !m_dock)
|
||||
return;
|
||||
|
||||
// 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 (!m_menuBar->parent())
|
||||
m_dock->top()->dock(CENTER, m_menuBar.get());
|
||||
m_dock->top()->dock(ui::CENTER, m_menuBar.get());
|
||||
}
|
||||
else {
|
||||
if (m_menuBar->parent())
|
||||
m_dock->undock(m_menuBar.get());
|
||||
if (m_menuBar->parent()) {
|
||||
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_notifications->setVisible(normal && m_notifications->hasNotifications());
|
||||
m_tabsBar->setVisible(normal);
|
||||
|
||||
// TODO set visibility of color bar widgets
|
||||
m_colorBar->setVisible(normal && isDoc);
|
||||
m_colorBarResizeConn = m_customizableDock->Resize.connect(
|
||||
[this] { saveColorBarConfiguration(); });
|
||||
|
||||
m_colorBarResizeConn = m_customizableDock->Resize.connect(&MainWindow::saveColorBarConfiguration,
|
||||
this);
|
||||
m_toolBar->setVisible(normal && isDoc);
|
||||
m_statusBar->setVisible(normal);
|
||||
m_contextBar->setVisible(isDoc && (m_mode == NormalMode || m_mode == ContextBarAndTimelineMode));
|
||||
|
||||
// Configure timeline
|
||||
{
|
||||
const gfx::Rect workspaceBounds = m_customizableDock->center()->center()->bounds();
|
||||
// Get legacy timeline position and splitter position
|
||||
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());
|
||||
|
||||
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()));
|
||||
if (m_timeline && m_timeline->parent())
|
||||
m_timelineResizeConn = dynamic_cast<Dock*>(m_timeline->parent())
|
||||
->Resize.connect(&MainWindow::saveTimelineConfiguration, this);
|
||||
|
||||
m_timeline->setVisible(isDoc && (m_mode == NormalMode || m_mode == ContextBarAndTimelineMode) &&
|
||||
pref.general.visibleTimeline());
|
||||
}
|
||||
|
||||
if (m_contextBar->isVisible()) {
|
||||
m_contextBar->updateForActiveTool();
|
||||
|
|
|
@ -95,7 +95,7 @@ public:
|
|||
void setDefaultLayout();
|
||||
void setMirroredDefaultLayout();
|
||||
void loadUserLayout(const Layout* layout);
|
||||
const Dock* customizableDock() const { return m_customizableDock; }
|
||||
Dock* customizableDock() { return m_customizableDock; }
|
||||
void setCustomizeDock(bool enable);
|
||||
|
||||
// When crash::DataRecovery finish to search for sessions, this
|
||||
|
|
|
@ -73,7 +73,8 @@ Tabs::~Tabs()
|
|||
m_addedTab.reset();
|
||||
m_removedTab.reset();
|
||||
|
||||
// Stop animation
|
||||
// Stop animation, can cause issues with docks when stopping during close.
|
||||
if (!is_app_state_closing())
|
||||
stopAnimation();
|
||||
|
||||
// Remove all tabs
|
||||
|
|
|
@ -20,6 +20,10 @@ public:
|
|||
WorkspaceTabs(TabsDelegate* tabsDelegate);
|
||||
~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; }
|
||||
void setPanel(WorkspacePanel* panel);
|
||||
|
||||
|
|
|
@ -38,6 +38,7 @@ public:
|
|||
|
||||
Items::iterator begin() { return m_items.begin(); }
|
||||
Items::iterator end() { return m_items.end(); }
|
||||
bool empty() const { return m_items.empty(); }
|
||||
|
||||
void setEditable(bool state);
|
||||
void setClickOpen(bool state);
|
||||
|
|
Loading…
Reference in New Issue