1// Aseprite
2// Copyright (C) 2018-2022 Igara Studio S.A.
3// Copyright (C) 2001-2018 David Capello
4//
5// This program is distributed under the terms of
6// the End-User License Agreement for Aseprite.
7
8#ifdef HAVE_CONFIG_H
9#include "config.h"
10#endif
11
12#include "app/ui/main_window.h"
13
14#include "app/app.h"
15#include "app/app_menus.h"
16#include "app/commands/command.h"
17#include "app/commands/commands.h"
18#include "app/crash/data_recovery.h"
19#include "app/i18n/strings.h"
20#include "app/ini_file.h"
21#include "app/modules/editors.h"
22#include "app/notification_delegate.h"
23#include "app/pref/preferences.h"
24#include "app/ui/browser_view.h"
25#include "app/ui/color_bar.h"
26#include "app/ui/context_bar.h"
27#include "app/ui/doc_view.h"
28#include "app/ui/editor/editor.h"
29#include "app/ui/editor/editor_view.h"
30#include "app/ui/home_view.h"
31#include "app/ui/main_menu_bar.h"
32#include "app/ui/notifications.h"
33#include "app/ui/preview_editor.h"
34#include "app/ui/skin/skin_property.h"
35#include "app/ui/skin/skin_theme.h"
36#include "app/ui/status_bar.h"
37#include "app/ui/timeline/timeline.h"
38#include "app/ui/toolbar.h"
39#include "app/ui/workspace.h"
40#include "app/ui/workspace_tabs.h"
41#include "app/ui_context.h"
42#include "base/fs.h"
43#include "os/system.h"
44#include "ui/message.h"
45#include "ui/splitter.h"
46#include "ui/system.h"
47#include "ui/tooltips.h"
48#include "ui/view.h"
49
50#ifdef ENABLE_SCRIPTING
51 #include "app/ui/devconsole_view.h"
52#endif
53
54namespace app {
55
56using namespace ui;
57
58class ScreenScalePanic : public INotificationDelegate {
59public:
60 std::string notificationText() override {
61 return "Reset Scale!";
62 }
63
64 void notificationClick() override {
65 auto& pref = Preferences::instance();
66
67 const int newScreenScale = 2;
68 const int newUIScale = 1;
69
70 if (pref.general.screenScale() != newScreenScale)
71 pref.general.screenScale(newScreenScale);
72
73 if (pref.general.uiScale() != newUIScale)
74 pref.general.uiScale(newUIScale);
75
76 pref.save();
77
78 ui::set_theme(ui::get_theme(), newUIScale);
79
80 Manager::getDefault()
81 ->updateAllDisplaysWithNewScale(newScreenScale);
82 }
83};
84
85MainWindow::MainWindow()
86 : m_mode(NormalMode)
87 , m_homeView(nullptr)
88 , m_scalePanic(nullptr)
89 , m_browserView(nullptr)
90#ifdef ENABLE_SCRIPTING
91 , m_devConsoleView(nullptr)
92#endif
93{
94 m_tooltipManager = new TooltipManager();
95 m_menuBar = new MainMenuBar();
96
97 // Register commands to load menus+shortcuts for these commands
98 Editor::registerCommands();
99
100 // Load all menus+keys for the first time
101 AppMenus::instance()->reload();
102
103 // Setup the main menubar
104 m_menuBar->setMenu(AppMenus::instance()->getRootMenu());
105
106 m_notifications = new Notifications();
107 m_statusBar = new StatusBar(m_tooltipManager);
108 m_colorBar = new ColorBar(colorBarPlaceholder()->align(),
109 m_tooltipManager);
110 m_contextBar = new ContextBar(m_tooltipManager, m_colorBar);
111 m_toolBar = new ToolBar();
112 m_tabsBar = new WorkspaceTabs(this);
113 m_workspace = new Workspace();
114 m_previewEditor = new PreviewEditorWindow();
115
116 // The timeline (AniControls) tooltips will use the keyboard
117 // shortcuts loaded above.
118 m_timeline = new Timeline(m_tooltipManager);
119
120 m_workspace->setTabsBar(m_tabsBar);
121 m_workspace->ActiveViewChanged.connect(&MainWindow::onActiveViewChange, this);
122
123 // configure all widgets to expansives
124 m_menuBar->setExpansive(true);
125 m_contextBar->setExpansive(true);
126 m_contextBar->setVisible(false);
127 m_statusBar->setExpansive(true);
128 m_colorBar->setExpansive(true);
129 m_toolBar->setExpansive(true);
130 m_tabsBar->setExpansive(true);
131 m_timeline->setExpansive(true);
132 m_workspace->setExpansive(true);
133 m_notifications->setVisible(false);
134
135 // Add the widgets in the boxes
136 addChild(m_tooltipManager);
137 menuBarPlaceholder()->addChild(m_menuBar);
138 menuBarPlaceholder()->addChild(m_notifications);
139 contextBarPlaceholder()->addChild(m_contextBar);
140 colorBarPlaceholder()->addChild(m_colorBar);
141 toolBarPlaceholder()->addChild(m_toolBar);
142 statusBarPlaceholder()->addChild(m_statusBar);
143 tabsPlaceholder()->addChild(m_tabsBar);
144 workspacePlaceholder()->addChild(m_workspace);
145 timelinePlaceholder()->addChild(m_timeline);
146
147 // Default splitter positions
148 colorBarSplitter()->setPosition(m_colorBar->sizeHint().w);
149 timelineSplitter()->setPosition(75);
150
151 // Reconfigure workspace when the timeline position is changed.
152 auto& pref = Preferences::instance();
153 pref.general.timelinePosition.AfterChange.connect([this]{ configureWorkspaceLayout(); });
154 pref.general.showMenuBar.AfterChange.connect([this]{ configureWorkspaceLayout(); });
155
156 // Prepare the window
157 remapWindow();
158
159 AppMenus::instance()->rebuildRecentList();
160
161 // When the language is change, we reload the menu bar strings and
162 // relayout the whole main window.
163 Strings::instance()->LanguageChange.connect([this] { onLanguageChange(); });
164}
165
166MainWindow::~MainWindow()
167{
168 delete m_scalePanic;
169
170#ifdef ENABLE_SCRIPTING
171 if (m_devConsoleView) {
172 if (m_devConsoleView->parent())
173 m_workspace->removeView(m_devConsoleView);
174 delete m_devConsoleView;
175 }
176#endif
177
178 if (m_browserView) {
179 if (m_browserView->parent())
180 m_workspace->removeView(m_browserView);
181 delete m_browserView;
182 }
183
184 if (m_homeView) {
185 if (m_homeView->parent())
186 m_workspace->removeView(m_homeView);
187 delete m_homeView;
188 }
189 delete m_contextBar;
190 delete m_previewEditor;
191
192 // Destroy the workspace first so ~Editor can dettach slots from
193 // ColorBar. TODO this is a terrible hack for slot/signal stuff,
194 // connections should be handle in a better/safer way.
195 delete m_workspace;
196
197 // Remove the root-menu from the menu-bar (because the rootmenu
198 // module should destroy it).
199 m_menuBar->setMenu(NULL);
200}
201
202void MainWindow::onLanguageChange()
203{
204 auto commands = Commands::instance();
205 std::vector<std::string> commandIDs;
206 commands->getAllIds(commandIDs);
207
208 for (const auto& commandID : commandIDs) {
209 Command* command = commands->byId(commandID.c_str());
210 command->generateFriendlyName();
211 }
212
213 m_menuBar->reload();
214 layout();
215 invalidate();
216}
217
218DocView* MainWindow::getDocView()
219{
220 return dynamic_cast<DocView*>(m_workspace->activeView());
221}
222
223HomeView* MainWindow::getHomeView()
224{
225 if (!m_homeView)
226 m_homeView = new HomeView;
227 return m_homeView;
228}
229
230#ifdef ENABLE_UPDATER
231CheckUpdateDelegate* MainWindow::getCheckUpdateDelegate()
232{
233 return getHomeView();
234}
235#endif
236
237#if ENABLE_SENTRY
238void MainWindow::updateConsentCheckbox()
239{
240 getHomeView()->updateConsentCheckbox();
241}
242#endif
243
244void MainWindow::showNotification(INotificationDelegate* del)
245{
246 m_notifications->addLink(del);
247 m_notifications->setVisible(true);
248 m_notifications->parent()->layout();
249}
250
251void MainWindow::showHomeOnOpen()
252{
253 // Don't open Home tab
254 if (!Preferences::instance().general.showHome()) {
255 configureWorkspaceLayout();
256 return;
257 }
258
259 if (!getHomeView()->parent()) {
260 TabView* selectedTab = m_tabsBar->getSelectedTab();
261
262 // Show "Home" tab in the first position, and select it only if
263 // there is no other view selected.
264 m_workspace->addView(m_homeView, 0);
265 if (selectedTab)
266 m_tabsBar->selectTab(selectedTab);
267 else
268 m_tabsBar->selectTab(m_homeView);
269 }
270}
271
272void MainWindow::showHome()
273{
274 if (!getHomeView()->parent()) {
275 m_workspace->addView(m_homeView, 0);
276 }
277 m_tabsBar->selectTab(m_homeView);
278}
279
280void MainWindow::showDefaultStatusBar()
281{
282 if (DocView* docView = getDocView())
283 m_statusBar->showDefaultText(docView->document());
284 else if (isHomeSelected())
285 m_statusBar->showAbout();
286 else
287 m_statusBar->clearText();
288}
289
290bool MainWindow::isHomeSelected() const
291{
292 return (m_homeView && m_workspace->activeView() == m_homeView);
293}
294
295void MainWindow::showBrowser(const std::string& filename,
296 const std::string& section)
297{
298 if (!m_browserView)
299 m_browserView = new BrowserView;
300
301 m_browserView->loadFile(filename, section);
302
303 if (!m_browserView->parent()) {
304 m_workspace->addView(m_browserView);
305 m_tabsBar->selectTab(m_browserView);
306 }
307}
308
309void MainWindow::showDevConsole()
310{
311#ifdef ENABLE_SCRIPTING
312 if (!m_devConsoleView)
313 m_devConsoleView = new DevConsoleView;
314
315 if (!m_devConsoleView->parent()) {
316 m_workspace->addView(m_devConsoleView);
317 m_tabsBar->selectTab(m_devConsoleView);
318 }
319#endif
320}
321
322void MainWindow::setMode(Mode mode)
323{
324 // Check if we already are in the given mode.
325 if (m_mode == mode)
326 return;
327
328 m_mode = mode;
329 configureWorkspaceLayout();
330}
331
332bool MainWindow::getTimelineVisibility() const
333{
334 return Preferences::instance().general.visibleTimeline();
335}
336
337void MainWindow::setTimelineVisibility(bool visible)
338{
339 Preferences::instance().general.visibleTimeline(visible);
340
341 configureWorkspaceLayout();
342}
343
344void MainWindow::popTimeline()
345{
346 Preferences& preferences = Preferences::instance();
347
348 if (!preferences.general.autoshowTimeline())
349 return;
350
351 if (!getTimelineVisibility())
352 setTimelineVisibility(true);
353}
354
355void MainWindow::dataRecoverySessionsAreReady()
356{
357 getHomeView()->dataRecoverySessionsAreReady();
358}
359
360bool MainWindow::onProcessMessage(ui::Message* msg)
361{
362 if (msg->type() == kOpenMessage)
363 showHomeOnOpen();
364
365 return Window::onProcessMessage(msg);
366}
367
368void MainWindow::onInitTheme(ui::InitThemeEvent& ev)
369{
370 app::gen::MainWindow::onInitTheme(ev);
371 if (m_previewEditor)
372 m_previewEditor->initTheme();
373}
374
375void MainWindow::onSaveLayout(SaveLayoutEvent& ev)
376{
377 // Invert the timeline splitter position before we save the setting.
378 if (Preferences::instance().general.timelinePosition() ==
379 gen::TimelinePosition::LEFT) {
380 timelineSplitter()->setPosition(100 - timelineSplitter()->getPosition());
381 }
382
383 Window::onSaveLayout(ev);
384}
385
386void MainWindow::onResize(ui::ResizeEvent& ev)
387{
388 app::gen::MainWindow::onResize(ev);
389
390 os::Window* nativeWindow = (display() ? display()->nativeWindow(): nullptr);
391 if (nativeWindow && nativeWindow->screen()) {
392 const int scale = nativeWindow->scale()*ui::guiscale();
393
394 // We can check for the available workarea to know that the user
395 // can resize the window to its full size and there will be enough
396 // room to display some common dialogs like (for example) the
397 // Preferences dialog.
398 if ((scale > 2) &&
399 (!m_scalePanic)) {
400 const gfx::Size wa = nativeWindow->screen()->workarea().size();
401 if ((wa.w / scale < 256 ||
402 wa.h / scale < 256)) {
403 showNotification(m_scalePanic = new ScreenScalePanic);
404 }
405 }
406 }
407}
408
409// When the active view is changed from methods like
410// Workspace::splitView(), this function is called, and we have to
411// inform to the UIContext that the current view has changed.
412void MainWindow::onActiveViewChange()
413{
414 // First we have to configure the MainWindow layout (e.g. show
415 // Timeline if needed) as UIContext::setActiveView() will configure
416 // several widgets (calling updateUsingEditor() functions) using the
417 // active document, and we need to know the available space on
418 // screen for each widget (e.g. the Timeline will configure its
419 // scrollable area/position depending on the number of
420 // layers/frames, but it needs to know its position on screen
421 // first).
422 configureWorkspaceLayout();
423
424 if (DocView* docView = getDocView())
425 UIContext::instance()->setActiveView(docView);
426 else
427 UIContext::instance()->setActiveView(nullptr);
428}
429
430bool MainWindow::isTabModified(Tabs* tabs, TabView* tabView)
431{
432 if (DocView* docView = dynamic_cast<DocView*>(tabView)) {
433 Doc* document = docView->document();
434 return document->isModified();
435 }
436 else {
437 return false;
438 }
439}
440
441bool MainWindow::canCloneTab(Tabs* tabs, TabView* tabView)
442{
443 ASSERT(tabView)
444
445 WorkspaceView* view = dynamic_cast<WorkspaceView*>(tabView);
446 return view->canCloneWorkspaceView();
447}
448
449void MainWindow::onSelectTab(Tabs* tabs, TabView* tabView)
450{
451 if (!tabView)
452 return;
453
454 WorkspaceView* view = dynamic_cast<WorkspaceView*>(tabView);
455 if (m_workspace->activeView() != view)
456 m_workspace->setActiveView(view);
457}
458
459void MainWindow::onCloseTab(Tabs* tabs, TabView* tabView)
460{
461 WorkspaceView* view = dynamic_cast<WorkspaceView*>(tabView);
462 ASSERT(view);
463 if (view)
464 m_workspace->closeView(view, false);
465}
466
467void MainWindow::onCloneTab(Tabs* tabs, TabView* tabView, int pos)
468{
469 EditorView::SetScrollUpdateMethod(EditorView::KeepOrigin);
470
471 WorkspaceView* view = dynamic_cast<WorkspaceView*>(tabView);
472 WorkspaceView* clone = view->cloneWorkspaceView();
473 ASSERT(clone);
474
475 m_workspace->addViewToPanel(
476 static_cast<WorkspaceTabs*>(tabs)->panel(), clone, true, pos);
477
478 clone->onClonedFrom(view);
479}
480
481void MainWindow::onContextMenuTab(Tabs* tabs, TabView* tabView)
482{
483 WorkspaceView* view = dynamic_cast<WorkspaceView*>(tabView);
484 ASSERT(view);
485 if (view)
486 view->onTabPopup(m_workspace);
487}
488
489void MainWindow::onTabsContainerDoubleClicked(Tabs* tabs)
490{
491 WorkspacePanel* mainPanel = m_workspace->mainPanel();
492 WorkspaceView* oldActiveView = mainPanel->activeView();
493 Doc* oldDoc = UIContext::instance()->activeDocument();
494
495 Command* command = Commands::instance()->byId(CommandId::NewFile());
496 UIContext::instance()->executeCommandFromMenuOrShortcut(command);
497
498 Doc* newDoc = UIContext::instance()->activeDocument();
499 if (newDoc != oldDoc) {
500 WorkspacePanel* doubleClickedPanel =
501 static_cast<WorkspaceTabs*>(tabs)->panel();
502
503 // TODO move this code to workspace?
504 // Put the new sprite in the double-clicked tabs control
505 if (doubleClickedPanel != mainPanel) {
506 WorkspaceView* newView = m_workspace->activeView();
507 m_workspace->removeView(newView);
508 m_workspace->addViewToPanel(doubleClickedPanel, newView, false, -1);
509
510 // Re-activate the old view in the main panel
511 mainPanel->setActiveView(oldActiveView);
512 doubleClickedPanel->setActiveView(newView);
513 }
514 }
515}
516
517void MainWindow::onMouseOverTab(Tabs* tabs, TabView* tabView)
518{
519 // Note: tabView can be NULL
520 if (DocView* docView = dynamic_cast<DocView*>(tabView))
521 m_statusBar->showDefaultText(docView->document());
522 else if (tabView)
523 m_statusBar->setStatusText(0, tabView->getTabText());
524 else
525 m_statusBar->showDefaultText();
526}
527
528void MainWindow::onMouseLeaveTab()
529{
530 m_statusBar->showDefaultText();
531}
532
533DropViewPreviewResult MainWindow::onFloatingTab(
534 Tabs* tabs,
535 TabView* tabView,
536 const gfx::Point& screenPos)
537{
538 return m_workspace->setDropViewPreview(
539 screenPos,
540 dynamic_cast<WorkspaceView*>(tabView),
541 static_cast<WorkspaceTabs*>(tabs));
542}
543
544void MainWindow::onDockingTab(Tabs* tabs, TabView* tabView)
545{
546 m_workspace->removeDropViewPreview();
547}
548
549DropTabResult MainWindow::onDropTab(Tabs* tabs,
550 TabView* tabView,
551 const gfx::Point& screenPos,
552 const bool clone)
553{
554 m_workspace->removeDropViewPreview();
555
556 DropViewAtResult result =
557 m_workspace->dropViewAt(screenPos,
558 dynamic_cast<WorkspaceView*>(tabView),
559 clone);
560
561 if (result == DropViewAtResult::MOVED_TO_OTHER_PANEL)
562 return DropTabResult::REMOVE;
563 else if (result == DropViewAtResult::CLONED_VIEW)
564 return DropTabResult::DONT_REMOVE;
565 else
566 return DropTabResult::NOT_HANDLED;
567}
568
569void MainWindow::configureWorkspaceLayout()
570{
571 const auto& pref = Preferences::instance();
572 bool normal = (m_mode == NormalMode);
573 bool isDoc = (getDocView() != nullptr);
574
575 if (os::instance()->menus() == nullptr ||
576 pref.general.showMenuBar()) {
577 m_menuBar->resetMaxSize();
578 }
579 else {
580 m_menuBar->setMaxSize(gfx::Size(0, 0));
581 }
582
583 m_menuBar->setVisible(normal);
584 m_notifications->setVisible(normal && m_notifications->hasNotifications());
585 m_tabsBar->setVisible(normal);
586 colorBarPlaceholder()->setVisible(normal && isDoc);
587 m_toolBar->setVisible(normal && isDoc);
588 m_statusBar->setVisible(normal);
589 m_contextBar->setVisible(
590 isDoc &&
591 (m_mode == NormalMode ||
592 m_mode == ContextBarAndTimelineMode));
593
594 // Configure timeline
595 {
596 auto timelinePosition = pref.general.timelinePosition();
597 bool invertWidgets = false;
598 int align = VERTICAL;
599 switch (timelinePosition) {
600 case gen::TimelinePosition::LEFT:
601 align = HORIZONTAL;
602 invertWidgets = true;
603 break;
604 case gen::TimelinePosition::RIGHT:
605 align = HORIZONTAL;
606 break;
607 case gen::TimelinePosition::BOTTOM:
608 break;
609 }
610
611 timelineSplitter()->setAlign(align);
612 timelinePlaceholder()->setVisible(
613 isDoc &&
614 (m_mode == NormalMode ||
615 m_mode == ContextBarAndTimelineMode) &&
616 pref.general.visibleTimeline());
617
618 bool invertSplitterPos = false;
619 if (invertWidgets) {
620 if (timelineSplitter()->firstChild() == workspacePlaceholder() &&
621 timelineSplitter()->lastChild() == timelinePlaceholder()) {
622 timelineSplitter()->removeChild(workspacePlaceholder());
623 timelineSplitter()->addChild(workspacePlaceholder());
624 invertSplitterPos = true;
625 }
626 }
627 else {
628 if (timelineSplitter()->firstChild() == timelinePlaceholder() &&
629 timelineSplitter()->lastChild() == workspacePlaceholder()) {
630 timelineSplitter()->removeChild(timelinePlaceholder());
631 timelineSplitter()->addChild(timelinePlaceholder());
632 invertSplitterPos = true;
633 }
634 }
635 if (invertSplitterPos)
636 timelineSplitter()->setPosition(100 - timelineSplitter()->getPosition());
637 }
638
639 if (m_contextBar->isVisible()) {
640 m_contextBar->updateForActiveTool();
641 }
642
643 layout();
644 invalidate();
645}
646
647} // namespace app
648