2021-10-13 21:50:42 +08:00
|
|
|
// Aseprite
|
2024-01-09 03:28:24 +08:00
|
|
|
// Copyright (C) 2021-2024 Igara Studio S.A.
|
2021-10-13 21:50:42 +08:00
|
|
|
//
|
|
|
|
// This program is distributed under the terms of
|
|
|
|
// the End-User License Agreement for Aseprite.
|
|
|
|
|
|
|
|
#ifdef HAVE_CONFIG_H
|
|
|
|
#include "config.h"
|
|
|
|
#endif
|
|
|
|
|
|
|
|
#include "app/ui/layout_selector.h"
|
|
|
|
|
|
|
|
#include "app/app.h"
|
2022-08-25 21:14:09 +08:00
|
|
|
#include "app/i18n/strings.h"
|
2022-08-26 22:02:06 +08:00
|
|
|
#include "app/match_words.h"
|
2022-08-25 21:14:09 +08:00
|
|
|
#include "app/ui/button_set.h"
|
|
|
|
#include "app/ui/configure_timeline_popup.h"
|
2021-10-13 21:50:42 +08:00
|
|
|
#include "app/ui/main_window.h"
|
|
|
|
#include "app/ui/separator_in_view.h"
|
|
|
|
#include "app/ui/skin/skin_theme.h"
|
2022-08-26 22:02:06 +08:00
|
|
|
#include "ui/entry.h"
|
2021-10-13 21:50:42 +08:00
|
|
|
#include "ui/listitem.h"
|
2022-08-25 21:14:09 +08:00
|
|
|
#include "ui/tooltips.h"
|
2021-10-13 21:50:42 +08:00
|
|
|
#include "ui/window.h"
|
|
|
|
|
2022-08-26 03:31:42 +08:00
|
|
|
#include "new_layout.xml.h"
|
|
|
|
|
2024-01-09 03:28:24 +08:00
|
|
|
#define ANI_TICKS 2
|
2021-10-13 21:50:42 +08:00
|
|
|
|
|
|
|
namespace app {
|
|
|
|
|
|
|
|
using namespace app::skin;
|
|
|
|
using namespace ui;
|
|
|
|
|
|
|
|
namespace {
|
|
|
|
|
2022-08-25 21:14:09 +08:00
|
|
|
// 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();
|
|
|
|
|
2022-08-25 21:25:41 +08:00
|
|
|
auto& timelinePosOption = Preferences::instance().general.timelinePosition;
|
|
|
|
|
|
|
|
setSelectedButtonFromTimelinePosition(timelinePosOption());
|
2022-08-26 03:31:42 +08:00
|
|
|
m_timelinePosConn = timelinePosOption.AfterChange.connect(
|
2022-08-25 21:25:41 +08:00
|
|
|
[this](gen::TimelinePosition position) { setSelectedButtonFromTimelinePosition(position); });
|
2022-08-25 21:14:09 +08:00
|
|
|
|
|
|
|
InitTheme.connect([this] {
|
|
|
|
auto theme = skin::SkinTheme::get(this);
|
|
|
|
setStyle(theme->styles.separatorInView());
|
|
|
|
});
|
|
|
|
initTheme();
|
|
|
|
}
|
|
|
|
|
|
|
|
private:
|
2022-08-25 21:25:41 +08:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2022-08-25 21:14:09 +08:00
|
|
|
void onItemChange(Item* item) override
|
|
|
|
{
|
|
|
|
ButtonSet::onItemChange(item);
|
|
|
|
ConfigureTimelinePopup::onChangeTimelinePosition(selectedItem());
|
2022-08-25 21:22:21 +08:00
|
|
|
|
|
|
|
// Show the timeline
|
2022-08-25 21:25:41 +08:00
|
|
|
App::instance()->mainWindow()->setTimelineVisibility(true);
|
2022-08-25 21:14:09 +08:00
|
|
|
}
|
2022-08-26 03:31:42 +08:00
|
|
|
|
|
|
|
obs::scoped_connection m_timelinePosConn;
|
2022-08-25 21:14:09 +08:00
|
|
|
};
|
|
|
|
|
2022-08-26 22:02:06 +08:00
|
|
|
// TODO this combobox is similar to FileSelector::CustomFileNameEntry
|
|
|
|
// and GotoFrameCommand::TagsEntry
|
|
|
|
class LayoutsEntry : public ComboBox {
|
|
|
|
public:
|
|
|
|
LayoutsEntry(Layouts& layouts) : m_layouts(layouts)
|
|
|
|
{
|
|
|
|
setEditable(true);
|
|
|
|
getEntryWidget()->Change.connect(&LayoutsEntry::onEntryChange, this);
|
|
|
|
fill(true);
|
|
|
|
}
|
|
|
|
|
|
|
|
private:
|
|
|
|
void fill(bool all)
|
|
|
|
{
|
|
|
|
deleteAllItems();
|
|
|
|
|
|
|
|
MatchWords match(getEntryWidget()->text());
|
|
|
|
|
|
|
|
bool matchAny = false;
|
|
|
|
for (auto& layout : m_layouts) {
|
|
|
|
if (match(layout->name())) {
|
|
|
|
matchAny = true;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for (auto& layout : m_layouts) {
|
|
|
|
if (all || !matchAny || match(layout->name()))
|
|
|
|
addItem(layout->name());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void onEntryChange()
|
|
|
|
{
|
|
|
|
closeListBox();
|
|
|
|
fill(false);
|
|
|
|
if (getItemCount() > 0)
|
|
|
|
openListBox();
|
|
|
|
}
|
|
|
|
|
|
|
|
Layouts& m_layouts;
|
|
|
|
};
|
|
|
|
|
2021-10-13 21:50:42 +08:00
|
|
|
}; // namespace
|
|
|
|
|
2022-08-26 03:31:42 +08:00
|
|
|
class LayoutSelector::LayoutItem : public ListItem {
|
|
|
|
public:
|
2024-01-12 03:43:46 +08:00
|
|
|
enum LayoutOption {
|
2022-08-26 03:31:42 +08:00
|
|
|
DEFAULT,
|
2024-01-12 03:43:46 +08:00
|
|
|
MIRRORED_DEFAULT,
|
2022-08-26 03:31:42 +08:00
|
|
|
USER_DEFINED,
|
2024-01-12 03:43:46 +08:00
|
|
|
NEW_LAYOUT,
|
2022-08-26 03:31:42 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
LayoutItem(LayoutSelector* selector,
|
2024-01-12 03:43:46 +08:00
|
|
|
const LayoutOption option,
|
2022-08-26 03:31:42 +08:00
|
|
|
const std::string& text,
|
2024-01-12 03:43:46 +08:00
|
|
|
const LayoutPtr layout)
|
2022-08-26 03:31:42 +08:00
|
|
|
: ListItem(text)
|
2024-01-12 03:43:46 +08:00
|
|
|
, m_option(option)
|
2022-08-26 22:02:06 +08:00
|
|
|
, m_selector(selector)
|
2022-08-26 03:31:42 +08:00
|
|
|
, m_layout(layout)
|
|
|
|
{
|
|
|
|
}
|
|
|
|
|
2024-01-12 03:43:46 +08:00
|
|
|
std::string getLayoutId() const
|
|
|
|
{
|
|
|
|
if (m_layout)
|
|
|
|
return m_layout->id();
|
|
|
|
else
|
|
|
|
return std::string();
|
|
|
|
}
|
|
|
|
|
|
|
|
bool matchId(const std::string& id) const { return (m_layout && m_layout->matchId(id)); }
|
|
|
|
|
|
|
|
const LayoutPtr& layout() const { return m_layout; }
|
2022-08-26 22:02:06 +08:00
|
|
|
|
|
|
|
void setLayout(const LayoutPtr& layout) { m_layout = layout; }
|
|
|
|
|
2022-08-26 03:31:42 +08:00
|
|
|
void selectImmediately()
|
|
|
|
{
|
|
|
|
MainWindow* win = App::instance()->mainWindow();
|
|
|
|
|
2024-01-12 03:43:46 +08:00
|
|
|
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;
|
2022-08-26 03:31:42 +08:00
|
|
|
}
|
2024-01-12 03:43:46 +08:00
|
|
|
// Even Default & Mirrored Default can have a customized layout
|
|
|
|
// (customized default layout).
|
|
|
|
if (m_layout)
|
|
|
|
win->loadUserLayout(m_layout.get());
|
2022-08-26 03:31:42 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
void selectAfterClose()
|
|
|
|
{
|
|
|
|
MainWindow* win = App::instance()->mainWindow();
|
|
|
|
|
2024-01-12 03:43:46 +08:00
|
|
|
switch (m_option) {
|
|
|
|
case LayoutOption::NEW_LAYOUT: {
|
2022-08-26 22:02:06 +08:00
|
|
|
// 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);
|
2022-08-26 03:31:42 +08:00
|
|
|
|
2022-08-26 22:02:06 +08:00
|
|
|
gen::NewLayout window;
|
|
|
|
LayoutsEntry name(m_selector->m_layouts);
|
|
|
|
name.getEntryWidget()->setMaxTextLength(128);
|
|
|
|
name.setFocusMagnet(true);
|
2024-01-12 03:43:46 +08:00
|
|
|
name.setValue(Strings::new_layout_default_name(m_selector->m_layouts.size() + 1));
|
2022-08-26 22:02:06 +08:00
|
|
|
|
|
|
|
window.namePlaceholder()->addChild(&name);
|
2022-08-26 03:31:42 +08:00
|
|
|
window.openWindowInForeground();
|
|
|
|
if (window.closer() == window.ok()) {
|
2024-01-12 03:43:46 +08:00
|
|
|
auto layout =
|
|
|
|
Layout::MakeFromDock(name.getValue(), name.getValue(), win->customizableDock());
|
2022-08-26 03:31:42 +08:00
|
|
|
|
2022-08-26 04:54:59 +08:00
|
|
|
m_selector->addLayout(layout);
|
2022-08-26 03:31:42 +08:00
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
default:
|
|
|
|
// Do nothing
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private:
|
2024-01-12 03:43:46 +08:00
|
|
|
LayoutOption m_option;
|
2022-08-26 03:31:42 +08:00
|
|
|
LayoutSelector* m_selector;
|
|
|
|
LayoutPtr m_layout;
|
|
|
|
};
|
|
|
|
|
2021-10-13 21:50:42 +08:00
|
|
|
void LayoutSelector::LayoutComboBox::onChange()
|
|
|
|
{
|
2022-08-26 03:31:42 +08:00
|
|
|
ComboBox::onChange();
|
2021-10-13 21:50:42 +08:00
|
|
|
if (auto item = dynamic_cast<LayoutItem*>(getSelectedItem())) {
|
2022-08-26 03:31:42 +08:00
|
|
|
item->selectImmediately();
|
|
|
|
m_selected = item;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void LayoutSelector::LayoutComboBox::onCloseListBox()
|
|
|
|
{
|
|
|
|
ComboBox::onCloseListBox();
|
|
|
|
if (m_selected) {
|
|
|
|
m_selected->selectAfterClose();
|
|
|
|
m_selected = nullptr;
|
2021-10-13 21:50:42 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-08-25 21:14:09 +08:00
|
|
|
LayoutSelector::LayoutSelector(TooltipManager* tooltipManager)
|
|
|
|
: m_button(SkinTheme::instance()->parts.iconUserData())
|
2021-10-13 21:50:42 +08:00
|
|
|
{
|
2024-01-12 03:43:46 +08:00
|
|
|
m_activeLayoutId = Preferences::instance().general.workspaceLayout();
|
|
|
|
|
2021-10-13 21:50:42 +08:00
|
|
|
m_button.Click.connect([this]() { switchSelector(); });
|
|
|
|
|
|
|
|
m_comboBox.setVisible(false);
|
|
|
|
|
|
|
|
addChild(&m_comboBox);
|
|
|
|
addChild(&m_button);
|
2022-08-25 21:14:09 +08:00
|
|
|
|
|
|
|
setupTooltips(tooltipManager);
|
|
|
|
|
|
|
|
InitTheme.connect([this] {
|
|
|
|
noBorderNoChildSpacing();
|
|
|
|
m_comboBox.noBorderNoChildSpacing();
|
|
|
|
m_button.noBorderNoChildSpacing();
|
|
|
|
});
|
|
|
|
initTheme();
|
2021-10-13 21:50:42 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
LayoutSelector::~LayoutSelector()
|
|
|
|
{
|
2024-01-12 03:43:46 +08:00
|
|
|
Preferences::instance().general.workspaceLayout(m_activeLayoutId);
|
|
|
|
|
2021-10-13 21:50:42 +08:00
|
|
|
stopAnimation();
|
|
|
|
}
|
|
|
|
|
2024-01-12 03:43:46 +08:00
|
|
|
LayoutPtr LayoutSelector::activeLayout()
|
|
|
|
{
|
|
|
|
return m_layouts.getById(m_activeLayoutId);
|
|
|
|
}
|
|
|
|
|
2022-08-26 03:31:42 +08:00
|
|
|
void LayoutSelector::addLayout(const LayoutPtr& layout)
|
|
|
|
{
|
2022-08-26 22:02:06 +08:00
|
|
|
bool added = m_layouts.addLayout(layout);
|
|
|
|
if (added) {
|
2024-01-12 03:43:46 +08:00
|
|
|
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);
|
2022-08-26 22:02:06 +08:00
|
|
|
}
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2022-08-26 03:31:42 +08:00
|
|
|
}
|
|
|
|
|
2024-01-12 03:43:46 +08:00
|
|
|
void LayoutSelector::updateActiveLayout(const LayoutPtr& newLayout)
|
|
|
|
{
|
|
|
|
bool result = m_layouts.addLayout(newLayout);
|
|
|
|
|
|
|
|
// 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);
|
|
|
|
}
|
|
|
|
|
2021-10-13 21:50:42 +08:00
|
|
|
void LayoutSelector::onAnimationFrame()
|
|
|
|
{
|
|
|
|
switch (animation()) {
|
|
|
|
case ANI_NONE: break;
|
|
|
|
case ANI_EXPANDING:
|
|
|
|
case ANI_COLLAPSING: {
|
|
|
|
const double t = animationTime();
|
2022-08-25 21:14:09 +08:00
|
|
|
m_comboBox.setSizeHint(gfx::Size(int(inbetween(m_startSize.w, m_endSize.w, t)),
|
|
|
|
int(inbetween(m_startSize.h, m_endSize.h, t))));
|
2021-10-13 21:50:42 +08:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (auto win = window())
|
|
|
|
win->layout();
|
|
|
|
}
|
|
|
|
|
|
|
|
void LayoutSelector::onAnimationStop(int animation)
|
|
|
|
{
|
|
|
|
switch (animation) {
|
2024-01-09 03:28:24 +08:00
|
|
|
case ANI_EXPANDING:
|
|
|
|
m_comboBox.setSizeHint(m_endSize);
|
|
|
|
if (m_switchComboBoxAfterAni) {
|
|
|
|
m_switchComboBoxAfterAni = false;
|
|
|
|
m_comboBox.openListBox();
|
|
|
|
}
|
|
|
|
break;
|
2021-10-13 21:50:42 +08:00
|
|
|
case ANI_COLLAPSING:
|
|
|
|
m_comboBox.setVisible(false);
|
|
|
|
m_comboBox.setSizeHint(m_endSize);
|
2024-01-09 03:28:24 +08:00
|
|
|
if (m_switchComboBoxAfterAni) {
|
|
|
|
m_switchComboBoxAfterAni = false;
|
|
|
|
m_comboBox.closeListBox();
|
|
|
|
}
|
2021-10-13 21:50:42 +08:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (auto win = window())
|
|
|
|
win->layout();
|
|
|
|
}
|
|
|
|
|
|
|
|
void LayoutSelector::switchSelector()
|
|
|
|
{
|
|
|
|
bool expand;
|
|
|
|
if (!m_comboBox.isVisible()) {
|
|
|
|
expand = true;
|
|
|
|
|
|
|
|
// Create the combobox for first time
|
|
|
|
if (m_comboBox.getItemCount() == 0) {
|
2024-01-12 03:43:46 +08:00
|
|
|
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));
|
2022-08-25 21:14:09 +08:00
|
|
|
m_comboBox.addItem(new TimelineButtons());
|
2024-01-12 03:43:46 +08:00
|
|
|
m_comboBox.addItem(new SeparatorInView(Strings::main_window_user_layouts(), HORIZONTAL));
|
2022-08-26 03:31:42 +08:00
|
|
|
for (const auto& layout : m_layouts) {
|
|
|
|
m_comboBox.addItem(new LayoutItem(this, LayoutItem::USER_DEFINED, layout->name(), layout));
|
|
|
|
}
|
2024-01-12 03:43:46 +08:00
|
|
|
m_comboBox.addItem(
|
|
|
|
new LayoutItem(this, LayoutItem::NEW_LAYOUT, Strings::main_window_new_layout(), nullptr));
|
2021-10-13 21:50:42 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
m_comboBox.setVisible(true);
|
|
|
|
m_comboBox.resetSizeHint();
|
|
|
|
m_startSize = gfx::Size(0, 0);
|
|
|
|
m_endSize = m_comboBox.sizeHint();
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
expand = false;
|
|
|
|
m_startSize = m_comboBox.bounds().size();
|
|
|
|
m_endSize = gfx::Size(0, 0);
|
|
|
|
}
|
|
|
|
|
2024-01-12 03:43:46 +08:00
|
|
|
if (auto item = getItemByLayoutId(m_activeLayoutId))
|
|
|
|
m_comboBox.setSelectedItem(item);
|
|
|
|
|
2021-10-13 21:50:42 +08:00
|
|
|
m_comboBox.setSizeHint(m_startSize);
|
|
|
|
startAnimation((expand ? ANI_EXPANDING : ANI_COLLAPSING), ANI_TICKS);
|
2024-12-23 21:08:47 +08:00
|
|
|
|
|
|
|
MainWindow* win = App::instance()->mainWindow();
|
|
|
|
win->setCustomizeDock(expand);
|
2021-10-13 21:50:42 +08:00
|
|
|
}
|
|
|
|
|
2024-01-09 03:28:24 +08:00
|
|
|
void LayoutSelector::switchSelectorFromCommand()
|
|
|
|
{
|
|
|
|
m_switchComboBoxAfterAni = true;
|
|
|
|
switchSelector();
|
|
|
|
}
|
|
|
|
|
|
|
|
bool LayoutSelector::isSelectorVisible() const
|
|
|
|
{
|
|
|
|
return (m_comboBox.isVisible());
|
|
|
|
}
|
|
|
|
|
2022-08-25 21:14:09 +08:00
|
|
|
void LayoutSelector::setupTooltips(TooltipManager* tooltipManager)
|
|
|
|
{
|
|
|
|
tooltipManager->addTooltipFor(&m_button, Strings::main_window_layout(), TOP);
|
|
|
|
}
|
|
|
|
|
2024-01-12 03:43:46 +08:00
|
|
|
LayoutSelector::LayoutItem* LayoutSelector::getItemByLayoutId(const std::string& id)
|
|
|
|
{
|
|
|
|
for (auto child : m_comboBox) {
|
|
|
|
if (auto item = dynamic_cast<LayoutItem*>(child)) {
|
|
|
|
if (item->matchId(id))
|
|
|
|
return item;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nullptr;
|
|
|
|
}
|
|
|
|
|
2021-10-13 21:50:42 +08:00
|
|
|
} // namespace app
|