1// Aseprite
2// Copyright (C) 2018-2022 Igara Studio S.A.
3// Copyright (C) 2001-2017 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/workspace_panel.h"
13
14#include "app/ui/skin/skin_theme.h"
15#include "app/ui/workspace.h"
16#include "app/ui/workspace_tabs.h"
17#include "app/ui/workspace_view.h"
18#include "base/remove_from_container.h"
19#include "ui/box.h"
20#include "ui/paint_event.h"
21#include "ui/resize_event.h"
22#include "ui/splitter.h"
23
24namespace app {
25
26#define ANI_DROPAREA_TICKS 4
27
28using namespace app::skin;
29using namespace ui;
30
31// static
32WidgetType WorkspacePanel::Type()
33{
34 static WidgetType type = kGenericWidget;
35 if (type == kGenericWidget)
36 type = register_widget_type();
37 return type;
38}
39
40WorkspacePanel::WorkspacePanel(PanelType panelType)
41 : Widget(WorkspacePanel::Type())
42 , m_panelType(panelType)
43 , m_tabs(nullptr)
44 , m_activeView(nullptr)
45 , m_dropArea(0)
46 , m_leftTime(0)
47 , m_rightTime(0)
48 , m_topTime(0)
49 , m_bottomTime(0)
50{
51 enableFlags(IGNORE_MOUSE);
52 InitTheme.connect(
53 [this]{
54 auto theme = SkinTheme::get(this);
55 setBgColor(theme->colors.workspace());
56 });
57 initTheme();
58}
59
60WorkspacePanel::~WorkspacePanel()
61{
62 // No views at this point.
63 ASSERT(m_views.empty());
64}
65
66void WorkspacePanel::setTabsBar(WorkspaceTabs* tabs)
67{
68 m_tabs = tabs;
69 m_tabs->setPanel(this);
70}
71
72void WorkspacePanel::addView(WorkspaceView* view, bool from_drop, int pos)
73{
74 if (pos < 0)
75 m_views.push_back(view);
76 else
77 m_views.insert(m_views.begin()+pos, view);
78
79 if (m_tabs)
80 m_tabs->addTab(dynamic_cast<TabView*>(view), from_drop, pos);
81
82 // Insert the view content as a hidden widget.
83 Widget* content = view->getContentWidget();
84 content->setVisible(false);
85 addChild(content);
86
87 setActiveView(view);
88}
89
90void WorkspacePanel::removeView(WorkspaceView* view)
91{
92 base::remove_from_container(m_views, view);
93
94 Widget* content = view->getContentWidget();
95 ASSERT(hasChild(content));
96 removeChild(content);
97
98 // Remove related tab.
99 if (m_tabs) {
100 m_tabs->removeTab(dynamic_cast<TabView*>(view), true);
101
102 // The selected
103 TabView* tabView = m_tabs->getSelectedTab();
104 view = dynamic_cast<WorkspaceView*>(tabView);
105 }
106 else
107 view = nullptr;
108
109 setActiveView(view);
110 if (!view)
111 getWorkspace()->setMainPanelAsActive();
112
113 // Destroy this panel
114 if (m_views.empty() && m_panelType == SUB_PANEL) {
115 Widget* self = parent();
116 ASSERT(self->type() == kBoxWidget);
117
118 Widget* splitter = self->parent();
119 ASSERT(splitter->type() == kSplitterWidget);
120
121 Widget* parent = splitter->parent();
122
123 Widget* side =
124 (splitter->firstChild() == self ?
125 splitter->lastChild():
126 splitter->firstChild());
127
128 splitter->removeChild(side);
129 parent->replaceChild(splitter, side);
130
131 self->deferDelete();
132
133 parent->layout();
134 }
135}
136
137WorkspaceView* WorkspacePanel::activeView()
138{
139 return m_activeView;
140}
141
142void WorkspacePanel::setActiveView(WorkspaceView* view)
143{
144 m_activeView = view;
145
146 for (auto v : m_views)
147 v->getContentWidget()->setVisible(v == view);
148
149 if (m_tabs && view)
150 m_tabs->selectTab(dynamic_cast<TabView*>(view));
151
152 adjustActiveViewBounds();
153
154 if (m_activeView)
155 m_activeView->onWorkspaceViewSelected();
156}
157
158void WorkspacePanel::onPaint(PaintEvent& ev)
159{
160 ev.graphics()->fillRect(bgColor(), clientBounds());
161}
162
163void WorkspacePanel::onResize(ui::ResizeEvent& ev)
164{
165 setBoundsQuietly(ev.bounds());
166 adjustActiveViewBounds();
167}
168
169void WorkspacePanel::adjustActiveViewBounds()
170{
171 gfx::Rect rc = childrenBounds();
172
173 // Preview to drop tabs in workspace
174 if (m_leftTime+m_topTime+m_rightTime+m_bottomTime > 1e-4) {
175 double left = double(m_leftTime) / double(ANI_DROPAREA_TICKS);
176 double top = double(m_topTime) / double(ANI_DROPAREA_TICKS);
177 double right = double(m_rightTime) / double(ANI_DROPAREA_TICKS);
178 double bottom = double(m_bottomTime) / double(ANI_DROPAREA_TICKS);
179 double threshold = getDropThreshold();
180
181 rc.x += int(inbetween(0.0, threshold, left));
182 rc.y += int(inbetween(0.0, threshold, top));
183 rc.w -= int(inbetween(0.0, threshold, left) + inbetween(0.0, threshold, right));
184 rc.h -= int(inbetween(0.0, threshold, top) + inbetween(0.0, threshold, bottom));
185 }
186
187 for (auto child : children())
188 if (child->isVisible())
189 child->setBounds(rc);
190}
191
192void WorkspacePanel::setDropViewPreview(const gfx::Point& screenPos,
193 WorkspaceView* view)
194{
195 int newDropArea = calculateDropArea(screenPos);
196 if (newDropArea != m_dropArea) {
197 m_dropArea = newDropArea;
198 startAnimation(ANI_DROPAREA, ANI_DROPAREA_TICKS);
199 }
200}
201
202void WorkspacePanel::removeDropViewPreview()
203{
204 if (m_dropArea) {
205 m_dropArea = 0;
206 startAnimation(ANI_DROPAREA, ANI_DROPAREA_TICKS);
207 }
208}
209
210void WorkspacePanel::onAnimationStop(int animation)
211{
212 if (animation == ANI_DROPAREA)
213 layout();
214}
215
216void WorkspacePanel::onAnimationFrame()
217{
218 if (animation() == ANI_DROPAREA) {
219 adjustTime(m_leftTime, LEFT);
220 adjustTime(m_topTime, TOP);
221 adjustTime(m_rightTime, RIGHT);
222 adjustTime(m_bottomTime, BOTTOM);
223 layout();
224 }
225}
226
227void WorkspacePanel::adjustTime(int& time, int flag)
228{
229 if (m_dropArea & flag) {
230 if (time < ANI_DROPAREA_TICKS)
231 ++time;
232 }
233 else if (time > 0)
234 --time;
235}
236
237DropViewAtResult WorkspacePanel::dropViewAt(const gfx::Point& screenPos,
238 WorkspacePanel* from,
239 WorkspaceView* view,
240 const bool clone)
241{
242 int dropArea = calculateDropArea(screenPos);
243 if (!dropArea)
244 return DropViewAtResult::NOTHING;
245
246 // If we're dropping the view in the same panel, and it's the only
247 // view in the panel: We cannot drop the view in the panel (because
248 // if we remove the last view, the panel will be destroyed).
249 if (!clone && from == this && m_views.size() == 1)
250 return DropViewAtResult::NOTHING;
251
252 int splitterAlign = 0;
253 if (dropArea & (LEFT | RIGHT)) splitterAlign = HORIZONTAL;
254 else if (dropArea & (TOP | BOTTOM)) splitterAlign = VERTICAL;
255
256 ASSERT(from);
257 DropViewAtResult result;
258 Workspace* workspace = getWorkspace();
259 WorkspaceView* originalView = view;
260 if (clone) {
261 view = view->cloneWorkspaceView();
262 result = DropViewAtResult::CLONED_VIEW;
263 }
264 else {
265 workspace->removeView(view);
266 result = DropViewAtResult::MOVED_TO_OTHER_PANEL;
267 }
268
269 WorkspaceTabs* newTabs = new WorkspaceTabs(m_tabs->getDelegate());
270 WorkspacePanel* newPanel = new WorkspacePanel(SUB_PANEL);
271 newTabs->setDockedStyle();
272 newPanel->setTabsBar(newTabs);
273 newPanel->setExpansive(true);
274
275 Widget* self = this;
276 VBox* side = new VBox;
277 side->InitTheme.connect(
278 [side]{
279 side->noBorderNoChildSpacing();
280 });
281 side->initTheme();
282 side->addChild(newTabs);
283 side->addChild(newPanel);
284
285 Splitter* splitter = new Splitter(Splitter::ByPercentage, splitterAlign);
286 splitter->setExpansive(true);
287 splitter->InitTheme.connect(
288 [splitter]{
289 auto theme = SkinTheme::get(splitter);
290 splitter->setStyle(theme->styles.workspaceSplitter());
291 });
292 splitter->initTheme();
293
294 Widget* parent = this->parent();
295 if (parent->type() == kBoxWidget) {
296 self = parent;
297 parent = self->parent();
298 ASSERT(parent->type() == kSplitterWidget);
299 }
300 if (parent->type() == Workspace::Type() ||
301 parent->type() == kSplitterWidget) {
302 parent->replaceChild(self, splitter);
303 }
304 else {
305 ASSERT(false);
306 }
307
308 double sideSpace;
309 if (m_panelType == MAIN_PANEL)
310 sideSpace = 30;
311 else
312 sideSpace = 50;
313
314 switch (dropArea) {
315 case LEFT:
316 case TOP:
317 splitter->setPosition(sideSpace);
318 splitter->addChild(side);
319 splitter->addChild(self);
320 break;
321 case RIGHT:
322 case BOTTOM:
323 splitter->setPosition(100-sideSpace);
324 splitter->addChild(self);
325 splitter->addChild(side);
326 break;
327 }
328
329 workspace->addViewToPanel(newPanel, view, true, -1);
330 parent->layout();
331
332 if (result == DropViewAtResult::CLONED_VIEW)
333 view->onClonedFrom(originalView);
334
335 return result;
336}
337
338int WorkspacePanel::calculateDropArea(const gfx::Point& screenPos) const
339{
340 const gfx::Point pos = display()->nativeWindow()->pointFromScreen(screenPos);
341 const gfx::Rect rc = childrenBounds();
342 if (rc.contains(pos)) {
343 int left = ABS(rc.x - pos.x);
344 int top = ABS(rc.y - pos.y);
345 int right = ABS(rc.x + rc.w - pos.x);
346 int bottom = ABS(rc.y + rc.h - pos.y);
347 int threshold = getDropThreshold();
348
349 if (left < threshold && left < right && left < top && left < bottom) {
350 return LEFT;
351 }
352 else if (top < threshold && top < left && top < right && top < bottom) {
353 return TOP;
354 }
355 else if (right < threshold && right < left && right < top && right < bottom) {
356 return RIGHT;
357 }
358 else if (bottom < threshold && bottom < left && bottom < top && bottom < right) {
359 return BOTTOM;
360 }
361 }
362 return 0;
363}
364
365int WorkspacePanel::getDropThreshold() const
366{
367 gfx::Rect cpos = childrenBounds();
368 int threshold = 32*guiscale();
369 if (threshold > cpos.w/2) threshold = cpos.w/2;
370 if (threshold > cpos.h/2) threshold = cpos.h/2;
371 return threshold;
372}
373
374Workspace* WorkspacePanel::getWorkspace()
375{
376 Widget* widget = this;
377 while (widget) {
378 if (widget->type() == Workspace::Type())
379 return static_cast<Workspace*>(widget);
380
381 widget = widget->parent();
382 }
383 return nullptr;
384}
385
386} // namespace app
387