1 // Aseprite UI Library
2 // Copyright (C) 2001-2017 David Capello
3 //
4 // This file is released under the terms of the MIT license.
5 // Read LICENSE.txt for more information.
6
7 #ifdef HAVE_CONFIG_H
8 #include "config.h"
9 #endif
10
11 #include "ui/window.h"
12
13 #include "gfx/size.h"
14 #include "ui/button.h"
15 #include "ui/graphics.h"
16 #include "ui/intern.h"
17 #include "ui/label.h"
18 #include "ui/manager.h"
19 #include "ui/message.h"
20 #include "ui/message_loop.h"
21 #include "ui/move_region.h"
22 #include "ui/resize_event.h"
23 #include "ui/size_hint_event.h"
24 #include "ui/system.h"
25 #include "ui/theme.h"
26
27 namespace ui {
28
29 using namespace gfx;
30
31 namespace {
32
33 enum {
34 WINDOW_NONE = 0,
35 WINDOW_MOVE = 1,
36 WINDOW_RESIZE_LEFT = 2,
37 WINDOW_RESIZE_RIGHT = 4,
38 WINDOW_RESIZE_TOP = 8,
39 WINDOW_RESIZE_BOTTOM = 16,
40 };
41
42 gfx::Point clickedMousePos;
43 gfx::Rect* clickedWindowPos = nullptr;
44
45 class WindowTitleLabel : public Label {
46 public:
WindowTitleLabel(const std::string & text)47 WindowTitleLabel(const std::string& text) : Label(text) {
48 setDecorative(true);
49 setType(kWindowTitleLabelWidget);
50 initTheme();
51 }
52 };
53
54
55 // Controls the "X" button in a window to close it.
56 class WindowCloseButton : public ButtonBase {
57 public:
WindowCloseButton()58 WindowCloseButton()
59 : ButtonBase("", kWindowCloseButtonWidget,
60 kButtonWidget, kButtonWidget) {
61 setDecorative(true);
62 initTheme();
63 }
64
65 protected:
66
onClick(Event & ev)67 void onClick(Event& ev) override {
68 ButtonBase::onClick(ev);
69 closeWindow();
70 }
71
onProcessMessage(Message * msg)72 bool onProcessMessage(Message* msg) override {
73 switch (msg->type()) {
74
75 case kSetCursorMessage:
76 ui::set_mouse_cursor(kArrowCursor);
77 return true;
78
79 case kKeyDownMessage:
80 if (window()->isForeground() &&
81 static_cast<KeyMessage*>(msg)->scancode() == kKeyEsc) {
82 setSelected(true);
83 return true;
84 }
85 break;
86
87 case kKeyUpMessage:
88 if (window()->isForeground() &&
89 static_cast<KeyMessage*>(msg)->scancode() == kKeyEsc) {
90 if (isSelected()) {
91 setSelected(false);
92 closeWindow();
93 return true;
94 }
95 }
96 break;
97 }
98
99 return ButtonBase::onProcessMessage(msg);
100 }
101 };
102
103 } // anonymous namespace
104
Window(Type type,const std::string & text)105 Window::Window(Type type, const std::string& text)
106 : Widget(kWindowWidget)
107 , m_closer(nullptr)
108 , m_titleLabel(nullptr)
109 , m_closeButton(nullptr)
110 , m_isDesktop(type == DesktopWindow)
111 , m_isMoveable(!m_isDesktop)
112 , m_isSizeable(!m_isDesktop)
113 , m_isOnTop(false)
114 , m_isWantFocus(true)
115 , m_isForeground(false)
116 , m_isAutoRemap(true)
117 {
118 setVisible(false);
119 setAlign(LEFT | MIDDLE);
120 if (type == WithTitleBar) {
121 setText(text);
122 addChild(m_closeButton = new WindowCloseButton);
123 }
124
125 initTheme();
126 }
127
~Window()128 Window::~Window()
129 {
130 manager()->_closeWindow(this, false);
131 }
132
setAutoRemap(bool state)133 void Window::setAutoRemap(bool state)
134 {
135 m_isAutoRemap = state;
136 }
137
setMoveable(bool state)138 void Window::setMoveable(bool state)
139 {
140 m_isMoveable = state;
141 }
142
setSizeable(bool state)143 void Window::setSizeable(bool state)
144 {
145 m_isSizeable = state;
146 }
147
setOnTop(bool state)148 void Window::setOnTop(bool state)
149 {
150 m_isOnTop = state;
151 }
152
setWantFocus(bool state)153 void Window::setWantFocus(bool state)
154 {
155 m_isWantFocus = state;
156 }
157
hitTest(const gfx::Point & point)158 HitTest Window::hitTest(const gfx::Point& point)
159 {
160 HitTestEvent ev(this, point, HitTestNowhere);
161 onHitTest(ev);
162 return ev.hit();
163 }
164
onClose(CloseEvent & ev)165 void Window::onClose(CloseEvent& ev)
166 {
167 // Fire Close signal
168 Close(ev);
169 }
170
onHitTest(HitTestEvent & ev)171 void Window::onHitTest(HitTestEvent& ev)
172 {
173 HitTest ht = HitTestNowhere;
174
175 // If this window is not movable or we are not completely visible.
176 if (!m_isMoveable) {
177 ev.setHit(ht);
178 return;
179 }
180
181 // TODO check why this is necessary, there should be a bug in
182 // the manager where we are receiving mouse events and are not
183 // the top most window.
184 Widget* picked = manager()->pick(ev.point());
185 if (picked &&
186 picked != this &&
187 picked->type() != kWindowTitleLabelWidget) {
188 ev.setHit(ht);
189 return;
190 }
191
192 int x = ev.point().x;
193 int y = ev.point().y;
194 gfx::Rect pos = bounds();
195 gfx::Rect cpos = childrenBounds();
196
197 // Move
198 if ((hasText())
199 && (((x >= cpos.x) &&
200 (x < cpos.x2()) &&
201 (y >= pos.y+border().bottom()) &&
202 (y < cpos.y)))) {
203 ht = HitTestCaption;
204 }
205 // Resize
206 else if (m_isSizeable) {
207 if ((x >= pos.x) && (x < cpos.x)) {
208 if ((y >= pos.y) && (y < cpos.y))
209 ht = HitTestBorderNW;
210 else if ((y > cpos.y2()-1) && (y <= pos.y2()-1))
211 ht = HitTestBorderSW;
212 else
213 ht = HitTestBorderW;
214 }
215 else if ((y >= pos.y) && (y < cpos.y)) {
216 if ((x >= pos.x) && (x < cpos.x))
217 ht = HitTestBorderNW;
218 else if ((x > cpos.x2()-1) && (x <= pos.x2()-1))
219 ht = HitTestBorderNE;
220 else
221 ht = HitTestBorderN;
222 }
223 else if ((x > cpos.x2()-1) && (x <= pos.x2()-1)) {
224 if ((y >= pos.y) && (y < cpos.y))
225 ht = HitTestBorderNE;
226 else if ((y > cpos.y2()-1) && (y <= pos.y2()-1))
227 ht = HitTestBorderSE;
228 else
229 ht = HitTestBorderE;
230 }
231 else if ((y > cpos.y2()-1) && (y <= pos.y2()-1)) {
232 if ((x >= pos.x) && (x < cpos.x))
233 ht = HitTestBorderSW;
234 else if ((x > cpos.x2()-1) && (x <= pos.x2()-1))
235 ht = HitTestBorderSE;
236 else
237 ht = HitTestBorderS;
238 }
239 }
240 else {
241 // Client area
242 ht = HitTestClient;
243 }
244
245 ev.setHit(ht);
246 }
247
onWindowResize()248 void Window::onWindowResize()
249 {
250 // Do nothing
251 }
252
onWindowMovement()253 void Window::onWindowMovement()
254 {
255 // Do nothing
256 }
257
remapWindow()258 void Window::remapWindow()
259 {
260 if (m_isAutoRemap) {
261 m_isAutoRemap = false;
262 this->setVisible(true);
263 }
264
265 setBounds(Rect(Point(bounds().x, bounds().y),
266 sizeHint()));
267
268 // load layout
269 loadLayout();
270
271 invalidate();
272 }
273
centerWindow()274 void Window::centerWindow()
275 {
276 Widget* manager = this->manager();
277
278 if (m_isAutoRemap)
279 remapWindow();
280
281 positionWindow(manager->bounds().w/2 - bounds().w/2,
282 manager->bounds().h/2 - bounds().h/2);
283 }
284
positionWindow(int x,int y)285 void Window::positionWindow(int x, int y)
286 {
287 if (m_isAutoRemap)
288 remapWindow();
289
290 setBounds(Rect(x, y, bounds().w, bounds().h));
291
292 invalidate();
293 }
294
moveWindow(const gfx::Rect & rect)295 void Window::moveWindow(const gfx::Rect& rect)
296 {
297 moveWindow(rect, true);
298 }
299
openWindow()300 void Window::openWindow()
301 {
302 if (!parent()) {
303 if (m_isAutoRemap)
304 centerWindow();
305
306 Manager::getDefault()->_openWindow(this);
307 }
308 }
309
openWindowInForeground()310 void Window::openWindowInForeground()
311 {
312 m_isForeground = true;
313
314 openWindow();
315
316 MessageLoop loop(manager());
317 while (!hasFlags(HIDDEN))
318 loop.pumpMessages();
319
320 m_isForeground = false;
321 }
322
closeWindow(Widget * closer)323 void Window::closeWindow(Widget* closer)
324 {
325 m_closer = closer;
326
327 manager()->_closeWindow(this, true);
328
329 // Close event
330 CloseEvent ev(closer);
331 onClose(ev);
332 }
333
isTopLevel()334 bool Window::isTopLevel()
335 {
336 Widget* manager = this->manager();
337 if (!manager->children().empty())
338 return (this == UI_FIRST_WIDGET(manager->children()));
339 else
340 return false;
341 }
342
onProcessMessage(Message * msg)343 bool Window::onProcessMessage(Message* msg)
344 {
345 switch (msg->type()) {
346
347 case kOpenMessage:
348 m_closer = NULL;
349 break;
350
351 case kCloseMessage:
352 saveLayout();
353 break;
354
355 case kMouseDownMessage: {
356 if (!m_isMoveable)
357 break;
358
359 clickedMousePos = static_cast<MouseMessage*>(msg)->position();
360 m_hitTest = hitTest(clickedMousePos);
361
362 if (m_hitTest != HitTestNowhere &&
363 m_hitTest != HitTestClient) {
364 if (clickedWindowPos == NULL)
365 clickedWindowPos = new gfx::Rect(bounds());
366 else
367 *clickedWindowPos = bounds();
368
369 captureMouse();
370 return true;
371 }
372 else
373 break;
374 }
375
376 case kMouseUpMessage:
377 if (hasCapture()) {
378 releaseMouse();
379 set_mouse_cursor(kArrowCursor);
380
381 if (clickedWindowPos != NULL) {
382 delete clickedWindowPos;
383 clickedWindowPos = NULL;
384 }
385
386 m_hitTest = HitTestNowhere;
387 return true;
388 }
389 break;
390
391 case kMouseMoveMessage:
392 if (!m_isMoveable)
393 break;
394
395 // Does it have the mouse captured?
396 if (hasCapture()) {
397 gfx::Point mousePos = static_cast<MouseMessage*>(msg)->position();
398
399 // Reposition/resize
400 if (m_hitTest == HitTestCaption) {
401 int x = clickedWindowPos->x + (mousePos.x - clickedMousePos.x);
402 int y = clickedWindowPos->y + (mousePos.y - clickedMousePos.y);
403 moveWindow(gfx::Rect(x, y,
404 bounds().w,
405 bounds().h), true);
406 }
407 else {
408 int x, y, w, h;
409
410 w = clickedWindowPos->w;
411 h = clickedWindowPos->h;
412
413 bool hitLeft = (m_hitTest == HitTestBorderNW ||
414 m_hitTest == HitTestBorderW ||
415 m_hitTest == HitTestBorderSW);
416 bool hitTop = (m_hitTest == HitTestBorderNW ||
417 m_hitTest == HitTestBorderN ||
418 m_hitTest == HitTestBorderNE);
419 bool hitRight = (m_hitTest == HitTestBorderNE ||
420 m_hitTest == HitTestBorderE ||
421 m_hitTest == HitTestBorderSE);
422 bool hitBottom = (m_hitTest == HitTestBorderSW ||
423 m_hitTest == HitTestBorderS ||
424 m_hitTest == HitTestBorderSE);
425
426 if (hitLeft) {
427 w += clickedMousePos.x - mousePos.x;
428 }
429 else if (hitRight) {
430 w += mousePos.x - clickedMousePos.x;
431 }
432
433 if (hitTop) {
434 h += (clickedMousePos.y - mousePos.y);
435 }
436 else if (hitBottom) {
437 h += (mousePos.y - clickedMousePos.y);
438 }
439
440 limitSize(&w, &h);
441
442 if ((bounds().w != w) ||
443 (bounds().h != h)) {
444 if (hitLeft)
445 x = clickedWindowPos->x - (w - clickedWindowPos->w);
446 else
447 x = bounds().x;
448
449 if (hitTop)
450 y = clickedWindowPos->y - (h - clickedWindowPos->h);
451 else
452 y = bounds().y;
453
454 moveWindow(gfx::Rect(x, y, w, h), false);
455 invalidate();
456 }
457 }
458 }
459 break;
460
461 case kSetCursorMessage:
462 if (m_isMoveable) {
463 gfx::Point mousePos = static_cast<MouseMessage*>(msg)->position();
464 HitTest ht = hitTest(mousePos);
465 CursorType cursor = kArrowCursor;
466
467 switch (ht) {
468 case HitTestCaption: cursor = kArrowCursor; break;
469 case HitTestBorderNW: cursor = kSizeNWCursor; break;
470 case HitTestBorderW: cursor = kSizeWCursor; break;
471 case HitTestBorderSW: cursor = kSizeSWCursor; break;
472 case HitTestBorderNE: cursor = kSizeNECursor; break;
473 case HitTestBorderE: cursor = kSizeECursor; break;
474 case HitTestBorderSE: cursor = kSizeSECursor; break;
475 case HitTestBorderN: cursor = kSizeNCursor; break;
476 case HitTestBorderS: cursor = kSizeSCursor; break;
477 }
478
479 set_mouse_cursor(cursor);
480 return true;
481 }
482 break;
483
484 }
485
486 return Widget::onProcessMessage(msg);
487 }
488
onResize(ResizeEvent & ev)489 void Window::onResize(ResizeEvent& ev)
490 {
491 windowSetPosition(ev.bounds());
492 }
493
onSizeHint(SizeHintEvent & ev)494 void Window::onSizeHint(SizeHintEvent& ev)
495 {
496 Widget* manager = this->manager();
497
498 if (m_isDesktop) {
499 Rect cpos = manager->childrenBounds();
500 ev.setSizeHint(cpos.w, cpos.h);
501 }
502 else {
503 Size maxSize(0, 0);
504 Size reqSize;
505
506 for (auto child : children()) {
507 if (!child->isDecorative()) {
508 reqSize = child->sizeHint();
509
510 maxSize.w = MAX(maxSize.w, reqSize.w);
511 maxSize.h = MAX(maxSize.h, reqSize.h);
512 }
513 }
514
515 if (m_titleLabel)
516 maxSize.w = MAX(maxSize.w, m_titleLabel->sizeHint().w);
517
518 ev.setSizeHint(maxSize.w + border().width(),
519 maxSize.h + border().height());
520 }
521 }
522
onBroadcastMouseMessage(WidgetsList & targets)523 void Window::onBroadcastMouseMessage(WidgetsList& targets)
524 {
525 targets.push_back(this);
526
527 // Continue sending the message to siblings windows until a desktop
528 // or foreground window.
529 if (isForeground() || isDesktop())
530 return;
531
532 Widget* sibling = nextSibling();
533 if (sibling)
534 sibling->broadcastMouseMessage(targets);
535 }
536
onSetText()537 void Window::onSetText()
538 {
539 onBuildTitleLabel();
540 Widget::onSetText();
541 initTheme();
542 }
543
onBuildTitleLabel()544 void Window::onBuildTitleLabel()
545 {
546 if (text().empty()) {
547 if (m_titleLabel) {
548 removeChild(m_titleLabel);
549 delete m_titleLabel;
550 m_titleLabel = nullptr;
551 }
552 }
553 else {
554 if (!m_titleLabel) {
555 m_titleLabel = new WindowTitleLabel(text());
556 addChild(m_titleLabel);
557 }
558 else {
559 m_titleLabel->setText(text());
560 m_titleLabel->setBounds(
561 gfx::Rect(m_titleLabel->bounds()).setSize(
562 m_titleLabel->sizeHint()));
563 }
564 }
565 }
566
windowSetPosition(const gfx::Rect & rect)567 void Window::windowSetPosition(const gfx::Rect& rect)
568 {
569 // Copy the new position rectangle
570 setBoundsQuietly(rect);
571 Rect cpos = childrenBounds();
572
573 // Set all the children to the same "cpos"
574 for (auto child : children()) {
575 if (child->isDecorative())
576 child->setDecorativeWidgetBounds();
577 else
578 child->setBounds(cpos);
579 }
580
581 onWindowResize();
582 }
583
limitSize(int * w,int * h)584 void Window::limitSize(int* w, int* h)
585 {
586 *w = MAX(*w, border().width());
587 *h = MAX(*h, border().height());
588 }
589
moveWindow(const gfx::Rect & rect,bool use_blit)590 void Window::moveWindow(const gfx::Rect& rect, bool use_blit)
591 {
592 #define FLAGS (DrawableRegionFlags)(kCutTopWindows | kUseChildArea)
593
594 Manager* manager = this->manager();
595 Message* msg;
596
597 manager->dispatchMessages();
598
599 // Get the window's current position
600 Rect old_pos = bounds();
601 int dx = rect.x - old_pos.x;
602 int dy = rect.y - old_pos.y;
603
604 // Get the manager's current position
605 Rect man_pos = manager->bounds();
606
607 // Send a kWinMoveMessage message to the window
608 msg = new Message(kWinMoveMessage);
609 msg->addRecipient(this);
610 manager->enqueueMessage(msg);
611
612 // Get the region & the drawable region of the window
613 Region oldDrawableRegion;
614 getDrawableRegion(oldDrawableRegion, FLAGS);
615
616 // If the size of the window changes...
617 if (old_pos.w != rect.w || old_pos.h != rect.h) {
618 // We have to change the position of all children.
619 windowSetPosition(rect);
620 }
621 else {
622 // We can just displace all the widgets by a delta (new_position -
623 // old_position)...
624 offsetWidgets(dx, dy);
625 }
626
627 // Get the new drawable region of the window (it's new because we
628 // moved the window to "rect")
629 Region newDrawableRegion;
630 getDrawableRegion(newDrawableRegion, FLAGS);
631
632 // First of all, we have to find the manager region to invalidate,
633 // it's the old window drawable region without the new window
634 // drawable region.
635 Region invalidManagerRegion;
636 invalidManagerRegion.createSubtraction(
637 oldDrawableRegion,
638 newDrawableRegion);
639
640 // In second place, we have to setup the window invalid region...
641
642 // If "use_blit" isn't activated, we have to redraw the whole window
643 // (sending kPaintMessage messages) in the new drawable region
644 if (!use_blit) {
645 invalidateRegion(newDrawableRegion);
646 }
647 // If "use_blit" is activated, we can move the old drawable to the
648 // new position (to redraw as little as possible).
649 else {
650 Region reg1;
651 reg1 = newDrawableRegion;
652 reg1.offset(-dx, -dy);
653
654 Region moveableRegion;
655 moveableRegion.createIntersection(oldDrawableRegion, reg1);
656
657 // Move the window's graphics
658 ScreenGraphics g;
659 hide_mouse_cursor();
660 {
661 IntersectClip clip(&g, man_pos);
662 if (clip) {
663 ui::move_region(manager, moveableRegion, dx, dy);
664 }
665 }
666 show_mouse_cursor();
667
668 reg1.createSubtraction(reg1, moveableRegion);
669 reg1.offset(dx, dy);
670 invalidateRegion(reg1);
671 }
672
673 manager->invalidateDisplayRegion(invalidManagerRegion);
674
675 onWindowMovement();
676 }
677
678 } // namespace ui
679