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