// Aseprite
// Copyright (C) 2018-2025  Igara Studio S.A.
// Copyright (C) 2001-2018  David Capello
//
// 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/main_window.h"

#include "app/app.h"
#include "app/app_menus.h"
#include "app/commands/command.h"
#include "app/commands/commands.h"
#include "app/crash/data_recovery.h"
#include "app/i18n/strings.h"
#include "app/ini_file.h"
#include "app/notification_delegate.h"
#include "app/pref/preferences.h"
#include "app/ui/browser_view.h"
#include "app/ui/color_bar.h"
#include "app/ui/context_bar.h"
#include "app/ui/doc_view.h"
#include "app/ui/dock.h"
#include "app/ui/editor/editor.h"
#include "app/ui/editor/editor_view.h"
#include "app/ui/home_view.h"
#include "app/ui/layout_selector.h"
#include "app/ui/main_menu_bar.h"
#include "app/ui/notifications.h"
#include "app/ui/preview_editor.h"
#include "app/ui/skin/skin_property.h"
#include "app/ui/skin/skin_theme.h"
#include "app/ui/status_bar.h"
#include "app/ui/timeline/timeline.h"
#include "app/ui/toolbar.h"
#include "app/ui/workspace.h"
#include "app/ui/workspace_tabs.h"
#include "app/ui_context.h"
#include "base/fs.h"
#include "os/event.h"
#include "os/event_queue.h"
#include "os/system.h"
#include "ui/app_state.h"
#include "ui/drag_event.h"
#include "ui/message.h"
#include "ui/splitter.h"
#include "ui/system.h"
#include "ui/tooltips.h"
#include "ui/view.h"

#ifdef ENABLE_SCRIPTING
  #include "app/ui/devconsole_view.h"
#endif

namespace app {

using namespace ui;

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:
  std::string notificationText() override { return "Reset Scale!"; }

  void notificationClick() override
  {
    auto& pref = Preferences::instance();

    const int newScreenScale = 2;
    const int newUIScale = 1;

    if (pref.general.screenScale() != newScreenScale)
      pref.general.screenScale(newScreenScale);

    if (pref.general.uiScale() != newUIScale)
      pref.general.uiScale(newUIScale);

    pref.save();

    ui::set_theme(ui::get_theme(), newUIScale);

    Manager::getDefault()->updateAllDisplays(newScreenScale, pref.general.gpuAcceleration());
  }
};

MainWindow::MainWindow()
  : ui::Window(ui::Window::DesktopWindow)
  , m_tooltipManager(new TooltipManager)
  , m_dock(new Dock)
  , m_customizableDock(new Dock)
  , m_mode(NormalMode)
  , m_homeView(nullptr)
  , m_scalePanic(nullptr)
  , m_browserView(nullptr)
#ifdef ENABLE_SCRIPTING
  , m_devConsoleView(nullptr)
#endif
{
  enableFlags(ALLOW_DROP);
  setNeedsTabletPressure(true);
}

// This 'initialize' function is a way to split the creation of the
// MainWindow. First a minimal instance of MainWindow is created, then
// all UI components that can trigger the Console to report any
// unexpected errors/warnings in the initialization. Prior to this,
// Aseprite could fail in the same constructor, and the Console didn't
// have access to the App::instance()->mainWindow() pointer.
//
// Refer to https://github.com/aseprite/aseprite/issues/3914
void MainWindow::initialize()
{
  m_menuBar = std::make_unique<MainMenuBar>();
  m_notifications = std::make_unique<Notifications>();
  m_layoutSelector = std::make_unique<LayoutSelector>(m_tooltipManager, m_notifications.get());

  // Register commands to load menus+shortcuts for these commands
  Editor::registerCommands();

  // Load all menus+keys for the first time
  AppMenus::instance()->reload();

  // Setup the main menubar
  m_menuBar->setMenu(AppMenus::instance()->getRootMenu());

  m_statusBar = std::make_unique<StatusBar>(m_tooltipManager);
  m_toolBar = std::make_unique<ToolBar>();
  m_tabsBar = std::make_unique<WorkspaceTabs>(this);
  m_workspace = std::make_unique<Workspace>();
  m_previewEditor = std::make_unique<PreviewEditorWindow>();
  m_colorBar = std::make_unique<ColorBar>(m_tooltipManager);
  m_contextBar = std::make_unique<ContextBar>(m_tooltipManager, m_colorBar.get());

  // The timeline (AniControls) tooltips will use the keyboard
  // shortcuts loaded above.
  m_timeline = std::make_unique<Timeline>(m_tooltipManager);

  m_workspace->setTabsBar(m_tabsBar.get());
  m_workspace->BeforeViewChanged.connect(&MainWindow::onBeforeViewChange, this);
  m_workspace->ActiveViewChanged.connect(&MainWindow::onActiveViewChange, this);

  // configure all widgets to expansives
  m_menuBar->setExpansive(true);
  m_contextBar->setExpansive(true);
  m_contextBar->setVisible(false);
  m_statusBar->setExpansive(true);
  m_colorBar->setExpansive(true);
  m_toolBar->setExpansive(true);
  m_tabsBar->setExpansive(true);
  m_timeline->setExpansive(true);
  m_workspace->setExpansive(true);
  m_notifications->setVisible(false);

  // IDs to create UI layouts from a Dock (see app::Layout
  // constructor).
  m_colorBar->setId("colorbar");
  m_contextBar->setId("contextbar");
  m_statusBar->setId("statusbar");
  m_timeline->setId("timeline");
  m_toolBar->setId("toolbar");
  m_workspace->setId("workspace");

  // Add the widgets in the boxes
  addChild(m_tooltipManager);
  addChild(m_dock);

  m_customizableDockPlaceholder = std::make_unique<Widget>();
  m_customizableDockPlaceholder->addChild(m_customizableDock);

  m_dock->top()->dock(ui::RIGHT, m_layoutSelector.get());
  m_dock->top()->center()->dock(ui::BOTTOM, m_tabsBar.get());
  m_dock->top()->center()->dock(ui::CENTER, m_menuBar.get());

  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(&MainWindow::saveActiveLayout,
                                                                     this);

  // Reconfigure workspace when the timeline position is changed.
  auto& pref = Preferences::instance();
  pref.general.timelinePosition.AfterChange.connect([this] { configureWorkspaceLayout(); });
  pref.general.showMenuBar.AfterChange.connect([this] { configureWorkspaceLayout(); });

  // Prepare the window
  remapWindow();

  AppMenus::instance()->rebuildRecentList();

  // When the language is changed, we reload the menu bar strings and
  // relayout the whole main window.
  Strings::instance()->LanguageChange.connect([this] { onLanguageChange(); });

  initTheme();
}

MainWindow::~MainWindow()
{
  m_timelineResizeConn.disconnect();
  m_colorBarResizeConn.disconnect();
  m_saveDockLayoutConn.disconnect();

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

#ifdef ENABLE_SCRIPTING
  if (m_devConsoleView) {
    if (m_devConsoleView->parent() && m_workspace)
      m_workspace->removeView(m_devConsoleView.get());
    m_devConsoleView.reset();
  }
#endif

  if (m_browserView) {
    if (m_browserView->parent() && m_workspace)
      m_workspace->removeView(m_browserView.get());
    m_browserView.reset();
  }

  if (m_homeView) {
    if (m_homeView->parent() && m_workspace)
      m_workspace->removeView(m_homeView.get());
    m_homeView.reset();
  }
  m_contextBar.reset();
  m_previewEditor.reset();

  // Destroy the workspace first so ~Editor can dettach slots from
  // ColorBar. TODO this is a terrible hack for slot/signal stuff,
  // connections should be handle in a better/safer way.
  m_workspace.reset();

  // Remove the root-menu from the menu-bar (because the rootmenu
  // module should destroy it).
  if (m_menuBar)
    m_menuBar->setMenu(nullptr);
}

void MainWindow::onLanguageChange()
{
  m_menuBar->reload();
  layout();
  invalidate();
}

DocView* MainWindow::getDocView()
{
  return dynamic_cast<DocView*>(m_workspace->activeView());
}

HomeView* MainWindow::getHomeView()
{
  if (!m_homeView)
    m_homeView = std::make_unique<HomeView>();
  return m_homeView.get();
}

#ifdef ENABLE_UPDATER
CheckUpdateDelegate* MainWindow::getCheckUpdateDelegate()
{
  return getHomeView();
}
#endif

#if ENABLE_SENTRY
void MainWindow::updateConsentCheckbox()
{
  getHomeView()->updateConsentCheckbox();
}
#endif

void MainWindow::showNotification(INotificationDelegate* del)
{
  m_notifications->addLink(del);
  m_notifications->setVisible(true);
  layout();
}

void MainWindow::showHomeOnOpen()
{
  // Don't open Home tab
  if (!Preferences::instance().general.showHome()) {
    configureWorkspaceLayout();
    return;
  }

  if (!getHomeView()->parent()) {
    TabView* selectedTab = m_tabsBar->getSelectedTab();

    // Show "Home" tab in the first position, and select it only if
    // there is no other view selected.
    m_workspace->addView(m_homeView.get(), 0);
    if (selectedTab)
      m_tabsBar->selectTab(selectedTab);
    else
      m_tabsBar->selectTab(m_homeView.get());
  }
}

void MainWindow::showHome()
{
  if (!getHomeView()->parent()) {
    m_workspace->addView(m_homeView.get(), 0);
  }
  m_tabsBar->selectTab(m_homeView.get());
}

void MainWindow::showDefaultStatusBar()
{
  if (DocView* docView = getDocView())
    m_statusBar->showDefaultText(docView->document());
  else if (isHomeSelected())
    m_statusBar->showAbout();
  else
    m_statusBar->clearText();
}

bool MainWindow::isHomeSelected() const
{
  return (m_homeView && m_workspace->activeView() == m_homeView.get());
}

void MainWindow::showBrowser(const std::string& filename, const std::string& section)
{
  if (!m_browserView)
    m_browserView = std::make_unique<BrowserView>();

  m_browserView->loadFile(filename, section);

  if (!m_browserView->parent()) {
    m_workspace->addView(m_browserView.get());
    m_tabsBar->selectTab(m_browserView.get());
  }
}

void MainWindow::showDevConsole()
{
#ifdef ENABLE_SCRIPTING
  if (!m_devConsoleView)
    m_devConsoleView = std::make_unique<DevConsoleView>();

  if (!m_devConsoleView->parent()) {
    m_workspace->addView(m_devConsoleView.get());
    m_tabsBar->selectTab(m_devConsoleView.get());
  }
#endif
}

void MainWindow::setMode(Mode mode)
{
  // Check if we already are in the given mode.
  if (m_mode == mode)
    return;

  m_mode = mode;
  configureWorkspaceLayout();
}

bool MainWindow::getTimelineVisibility() const
{
  return Preferences::instance().general.visibleTimeline();
}

void MainWindow::setTimelineVisibility(bool visible)
{
  Preferences::instance().general.visibleTimeline(visible);

  configureWorkspaceLayout();
}

void MainWindow::popTimeline()
{
  if (!Preferences::instance().general.autoshowTimeline())
    return;

  if (!getTimelineVisibility())
    setTimelineVisibility(true);
}

void MainWindow::setDefaultLayout()
{
  m_timelineResizeConn.disconnect();
  m_colorBarResizeConn.disconnect();

  const auto colorBarWidth = get_config_double(kLegacyLayoutMainWindowSection,
                                               kLegacyLayoutColorBarSplitter,
                                               m_colorBar->sizeHint().w);

  m_customizableDock->resetDocks();
  m_customizableDock->dock(ui::LEFT, m_colorBar.get(), gfx::Size(colorBarWidth, 0));
  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());

  const auto timelineSplitterPos =
    get_config_double(kLegacyLayoutMainWindowSection, kLegacyLayoutTimelineSplitter, 75.0) / 100.0;
  const auto timelinePos = Preferences::instance().general.timelinePosition();

  // We calculate a estimate of the workspace bounds (as we don't yet
  // know its size, because we're just constructing the dock where the
  // workspace will be inside).
  const int kLegacySplitterSeparation = 3 * ui::guiscale();
  auto workspaceBounds = bounds();
  workspaceBounds.w -= colorBarWidth + m_toolBar->sizeHint().w + 2 * kLegacySplitterSeparation;
  workspaceBounds.h -= m_menuBar->sizeHint().h + m_tabsBar->sizeHint().h +
                       m_contextBar->sizeHint().h + m_statusBar->sizeHint().h;

  int timelineSide;
  gfx::Size timelineSize(75, 75);
  switch (timelinePos) {
    case gen::TimelinePosition::LEFT:
      timelineSide = ui::LEFT;
      timelineSize.w = (workspaceBounds.w * (1.0 - timelineSplitterPos));
      break;
    case gen::TimelinePosition::RIGHT:
      timelineSide = ui::RIGHT;
      timelineSize.w = (workspaceBounds.w * (1.0 - timelineSplitterPos));
      break;
    default:
    case gen::TimelinePosition::BOTTOM:
      timelineSide = ui::BOTTOM;
      timelineSize.h = (workspaceBounds.h * (1.0 - timelineSplitterPos));
      break;
  }

  // Timeline config
  m_customizableDock->center()->center()->dock(timelineSide,
                                               m_timeline.get(),
                                               timelineSize.createUnion(gfx::Size(64, 64)));

  m_customizableDock->center()->center()->dock(ui::CENTER, m_workspace.get());
  configureWorkspaceLayout();
}

void MainWindow::setMirroredDefaultLayout()
{
  m_timelineResizeConn.disconnect();
  m_colorBarResizeConn.disconnect();

  auto colorBarWidth = get_config_double(kLegacyLayoutMainWindowSection,
                                         kLegacyLayoutColorBarSplitter,
                                         m_colorBar->sizeHint().w);

  m_customizableDock->resetDocks();
  m_customizableDock->dock(ui::RIGHT, m_colorBar.get(), gfx::Size(colorBarWidth, 0));
  m_customizableDock->dock(ui::BOTTOM, m_statusBar.get());
  m_customizableDock->center()->dock(ui::TOP, m_contextBar.get());
  m_customizableDock->center()->dock(ui::LEFT, m_toolBar.get());
  m_customizableDock->center()->center()->dock(ui::BOTTOM,
                                               m_timeline.get(),
                                               gfx::Size(64 * guiscale(), 64 * guiscale()));
  m_customizableDock->center()->center()->dock(ui::CENTER, m_workspace.get());
  configureWorkspaceLayout();
}

void MainWindow::loadUserLayout(const Layout* layout)
{
  m_timelineResizeConn.disconnect();
  m_colorBarResizeConn.disconnect();

  m_customizableDock->resetDocks();

  if (!layout->loadLayout(m_customizableDock)) {
    LOG(WARNING, "Layout %s failed to load, resetting to default.\n", layout->id().c_str());
    setDefaultLayout();
  }

  this->layout();
}

void MainWindow::setCustomizeDock(bool enable)
{
  m_customizableDock->setCustomizing(enable);
}

void MainWindow::dataRecoverySessionsAreReady()
{
  getHomeView()->dataRecoverySessionsAreReady();
}

bool MainWindow::onProcessMessage(ui::Message* msg)
{
  if (msg->type() == kOpenMessage)
    showHomeOnOpen();

  return Window::onProcessMessage(msg);
}

void MainWindow::onInitTheme(ui::InitThemeEvent& ev)
{
  ui::Window::onInitTheme(ev);
  noBorderNoChildSpacing();
  if (m_previewEditor)
    m_previewEditor->initTheme();

  auto* theme = static_cast<skin::SkinTheme*>(this->theme());
  m_dock->setBgColor(theme->colors.windowFace());
  m_customizableDock->setBgColor(theme->colors.workspace());
}

void MainWindow::onResize(ui::ResizeEvent& ev)
{
  ui::Window::onResize(ev);

  // Load default or user-selected layout after the first resize event
  // is received.
  if (m_firstResize) {
    m_firstResize = false;

    // If the layout is defined in the user layouts file, we loaded it
    // (it can be a modified default/mirrored layout).
    if (LayoutPtr layout = m_layoutSelector->activeLayout()) {
      loadUserLayout(layout.get());
    }
    else if (m_layoutSelector->activeLayoutId() == Layout::kMirroredDefault) {
      setMirroredDefaultLayout();
    }
    else {
      setDefaultLayout();
    }
  }

  os::Window* nativeWindow = (display() ? display()->nativeWindow() : nullptr);
  if (nativeWindow && nativeWindow->screen()) {
    const int scale = nativeWindow->scale() * ui::guiscale();

    // We can check for the available workarea to know that the user
    // can resize the window to its full size and there will be enough
    // room to display some common dialogs like (for example) the
    // Preferences dialog.
    if ((scale > 2) && (!m_scalePanic)) {
      const gfx::Size wa = nativeWindow->screen()->workarea().size();
      if ((wa.w / scale < 256 || wa.h / scale < 256)) {
        m_scalePanic = std::make_unique<ScreenScalePanic>();
        showNotification(m_scalePanic.get());
      }
    }
  }
}

void MainWindow::onBeforeViewChange()
{
  UIContext::instance()->notifyBeforeActiveSiteChanged();
}

// When the active view is changed from methods like
// Workspace::splitView(), this function is called, and we have to
// inform to the UIContext that the current view has changed.
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 || !m_dock)
    return;

  // First we have to configure the MainWindow layout (e.g. show
  // Timeline if needed) as UIContext::setActiveView() will configure
  // several widgets (calling updateUsingEditor() functions) using the
  // active document, and we need to know the available space on
  // screen for each widget (e.g. the Timeline will configure its
  // scrollable area/position depending on the number of
  // layers/frames, but it needs to know its position on screen
  // first).
  configureWorkspaceLayout();

  if (DocView* docView = getDocView())
    UIContext::instance()->setActiveView(docView);
  else
    UIContext::instance()->setActiveView(nullptr);
}

void MainWindow::onDrop(ui::DragEvent& e)
{
  if (e.hasImage() && !e.hasPaths()) {
    auto* cmd = Commands::instance()->byId(CommandId::NewFile());
    Params params;
    params.set("fromDraggedData", "true");
    UIContext::instance()->setDraggedData(std::make_unique<DraggedData>(e.getImage()));
    UIContext::instance()->executeCommand(cmd, params);
    e.handled(true);
    invalidate();
    flushRedraw();
    os::Event ev;
    os::System::instance()->eventQueue()->queueEvent(ev);
  }
}

bool MainWindow::isTabModified(Tabs* tabs, TabView* tabView)
{
  if (DocView* docView = dynamic_cast<DocView*>(tabView)) {
    Doc* document = docView->document();
    return document->isModified();
  }
  else {
    return false;
  }
}

bool MainWindow::canCloneTab(Tabs* tabs, TabView* tabView)
{
  ASSERT(tabView)

  WorkspaceView* view = dynamic_cast<WorkspaceView*>(tabView);
  return view->canCloneWorkspaceView();
}

void MainWindow::onSelectTab(Tabs* tabs, TabView* tabView)
{
  if (!tabView)
    return;

  WorkspaceView* view = dynamic_cast<WorkspaceView*>(tabView);
  if (m_workspace->activeView() != view)
    m_workspace->setActiveView(view);
}

void MainWindow::onCloseTab(Tabs* tabs, TabView* tabView)
{
  WorkspaceView* view = dynamic_cast<WorkspaceView*>(tabView);
  ASSERT(view);
  if (view)
    m_workspace->closeView(view, false);
}

void MainWindow::onCloneTab(Tabs* tabs, TabView* tabView, int pos)
{
  EditorView::SetScrollUpdateMethod(EditorView::KeepOrigin);

  WorkspaceView* view = dynamic_cast<WorkspaceView*>(tabView);
  WorkspaceView* clone = view->cloneWorkspaceView();
  ASSERT(clone);

  m_workspace->addViewToPanel(static_cast<WorkspaceTabs*>(tabs)->panel(), clone, true, pos);

  clone->onClonedFrom(view);
}

void MainWindow::onContextMenuTab(Tabs* tabs, TabView* tabView)
{
  WorkspaceView* view = dynamic_cast<WorkspaceView*>(tabView);
  ASSERT(view);
  if (view)
    view->onTabPopup(m_workspace.get());
}

void MainWindow::onTabsContainerDoubleClicked(Tabs* tabs)
{
  WorkspacePanel* mainPanel = m_workspace->mainPanel();
  WorkspaceView* oldActiveView = mainPanel->activeView();
  Doc* oldDoc = UIContext::instance()->activeDocument();

  Command* command = Commands::instance()->byId(CommandId::NewFile());
  UIContext::instance()->executeCommandFromMenuOrShortcut(command);

  Doc* newDoc = UIContext::instance()->activeDocument();
  if (newDoc != oldDoc) {
    WorkspacePanel* doubleClickedPanel = static_cast<WorkspaceTabs*>(tabs)->panel();

    // TODO move this code to workspace?
    // Put the new sprite in the double-clicked tabs control
    if (doubleClickedPanel != mainPanel) {
      WorkspaceView* newView = m_workspace->activeView();
      m_workspace->removeView(newView);
      m_workspace->addViewToPanel(doubleClickedPanel, newView, false, -1);

      // Re-activate the old view in the main panel
      mainPanel->setActiveView(oldActiveView);
      doubleClickedPanel->setActiveView(newView);
    }
  }
}

void MainWindow::onMouseOverTab(Tabs* tabs, TabView* tabView)
{
  // Note: tabView can be NULL
  if (DocView* docView = dynamic_cast<DocView*>(tabView))
    m_statusBar->showDefaultText(docView->document());
  else if (tabView)
    m_statusBar->setStatusText(0, tabView->getTabText());
  else
    m_statusBar->showDefaultText();
}

void MainWindow::onMouseLeaveTab()
{
  m_statusBar->showDefaultText();
}

DropViewPreviewResult MainWindow::onFloatingTab(Tabs* tabs,
                                                TabView* tabView,
                                                const gfx::Point& screenPos)
{
  return m_workspace->setDropViewPreview(screenPos,
                                         dynamic_cast<WorkspaceView*>(tabView),
                                         static_cast<WorkspaceTabs*>(tabs));
}

void MainWindow::onDockingTab(Tabs* tabs, TabView* tabView)
{
  m_workspace->removeDropViewPreview();
}

DropTabResult MainWindow::onDropTab(Tabs* tabs,
                                    TabView* tabView,
                                    const gfx::Point& screenPos,
                                    const bool clone)
{
  m_workspace->removeDropViewPreview();

  DropViewAtResult result =
    m_workspace->dropViewAt(screenPos, dynamic_cast<WorkspaceView*>(tabView), clone);

  if (result == DropViewAtResult::MOVED_TO_OTHER_PANEL)
    return DropTabResult::REMOVE;
  else if (result == DropViewAtResult::CLONED_VIEW)
    return DropTabResult::DONT_REMOVE;
  else
    return DropTabResult::NOT_HANDLED;
}

void MainWindow::configureWorkspaceLayout()
{
  // First layout to get the bounds of some widgets
  layout();

  const auto& pref = Preferences::instance();
  bool normal = (m_mode == NormalMode);
  bool showMenu = normal;
  bool isDoc = (getDocView() != nullptr);

  if (os::System::instance()->menus() && !pref.general.showMenuBar())
    showMenu = false;

  m_menuBar->setVisible(showMenu);
  m_notifications->setVisible(normal && m_notifications->hasNotifications());
  m_tabsBar->setVisible(normal);
  m_colorBar->setVisible(normal && isDoc);
  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
  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();
  }

  layout();
}

void MainWindow::saveTimelineConfiguration()
{
  const auto& pref = Preferences::instance();
  const gfx::Rect timelineBounds = m_timeline->bounds();
  const gfx::Rect workspaceBounds = m_customizableDock->center()->center()->bounds();
  auto timelinePosition = pref.general.timelinePosition();
  double timelineSplitterPos = 0.75;

  switch (timelinePosition) {
    case gen::TimelinePosition::LEFT:
    case gen::TimelinePosition::RIGHT:
      timelineSplitterPos = 1.0 - double(timelineBounds.w) / workspaceBounds.w;
      break;
    case gen::TimelinePosition::BOTTOM:
      timelineSplitterPos = 1.0 - double(timelineBounds.h) / workspaceBounds.h;
      break;
  }

  set_config_double(kLegacyLayoutMainWindowSection,
                    kLegacyLayoutTimelineSplitter,
                    std::clamp(timelineSplitterPos * 100.0, 1.0, 99.0));
}

void MainWindow::saveColorBarConfiguration()
{
  set_config_double(kLegacyLayoutMainWindowSection,
                    kLegacyLayoutColorBarSplitter,
                    m_colorBar->bounds().w);
}

void MainWindow::saveActiveLayout()
{
  ASSERT(m_layoutSelector);

  auto id = m_layoutSelector->activeLayoutId();
  auto layout = Layout::MakeFromDock(id, id, m_customizableDock);
  m_layoutSelector->updateActiveLayout(layout);
}

} // namespace app
