1// Aseprite UI Library
2// Copyright (C) 2020-2021 Igara Studio S.A.
3// Copyright (C) 2001-2017 David Capello
4//
5// This file is released under the terms of the MIT license.
6// Read LICENSE.txt for more information.
7
8#ifdef HAVE_CONFIG_H
9#include "config.h"
10#endif
11
12#include "gfx/size.h"
13#include "ui/graphics.h"
14#include "ui/intern.h"
15#include "ui/size_hint_event.h"
16#include "ui/theme.h"
17#include "ui/ui.h"
18
19namespace ui {
20
21using namespace gfx;
22
23PopupWindow::PopupWindow(const std::string& text,
24 const ClickBehavior clickBehavior,
25 const EnterBehavior enterBehavior,
26 const bool withCloseButton)
27 : Window(text.empty() ? WithoutTitleBar: WithTitleBar, text)
28 , m_clickBehavior(clickBehavior)
29 , m_enterBehavior(enterBehavior)
30{
31 setSizeable(false);
32 setMoveable(false);
33 setWantFocus(false);
34 setAlign(LEFT | TOP);
35
36 if (!withCloseButton) {
37 // Remove close button
38 for (auto child : children()) {
39 if (child->type() == kWindowCloseButtonWidget) {
40 delete child;
41 break;
42 }
43 }
44 }
45
46 initTheme();
47}
48
49PopupWindow::~PopupWindow()
50{
51 stopFilteringMessages();
52}
53
54void PopupWindow::setHotRegion(const gfx::Region& screenRegion)
55{
56 startFilteringMessages();
57
58 m_hotRegion = screenRegion;
59}
60
61void PopupWindow::setClickBehavior(ClickBehavior behavior)
62{
63 m_clickBehavior = behavior;
64}
65
66void PopupWindow::setEnterBehavior(EnterBehavior behavior)
67{
68 m_enterBehavior = behavior;
69}
70
71void PopupWindow::makeFloating()
72{
73 stopFilteringMessages();
74 setMoveable(true);
75 m_fixed = false;
76
77 onMakeFloating();
78}
79
80void PopupWindow::makeFixed()
81{
82 startFilteringMessages();
83 setMoveable(false);
84 m_fixed = true;
85
86 onMakeFixed();
87}
88
89bool PopupWindow::onProcessMessage(Message* msg)
90{
91 switch (msg->type()) {
92
93 // There are cases where startFilteringMessages() is called when a
94 // kCloseMessage for this same PopupWindow is enqueued. Processing
95 // the kOpenMessage we ensure that the popup will be filtering
96 // messages if it's needed when it's visible (as kCloseMessage and
97 // kOpenMessage must be enqueued in the correct order).
98 case kOpenMessage:
99 if (!isMoveable())
100 startFilteringMessages();
101 break;
102
103 case kCloseMessage:
104 stopFilteringMessages();
105 break;
106
107 case kMouseLeaveMessage:
108 if (m_hotRegion.isEmpty() && m_fixed)
109 closeWindow(nullptr);
110 break;
111
112 case kKeyDownMessage:
113 if (m_filtering) {
114 KeyMessage* keymsg = static_cast<KeyMessage*>(msg);
115 KeyScancode scancode = keymsg->scancode();
116
117 if (scancode == kKeyEsc)
118 closeWindow(nullptr);
119
120 if (m_enterBehavior == EnterBehavior::CloseOnEnter &&
121 (scancode == kKeyEnter ||
122 scancode == kKeyEnterPad)) {
123 closeWindow(this);
124 return true;
125 }
126
127 // If the message came from a filter, we don't send it back to
128 // the default Window processing (which will send the message
129 // to the Manager). In this way, the focused children can
130 // process the kKeyDownMessage.
131 if (msg->fromFilter())
132 return false;
133 }
134 break;
135
136 case kMouseDownMessage:
137 if (m_filtering && msg->display()) {
138 auto mouseMsg = static_cast<const MouseMessage*>(msg);
139 gfx::Point screenPos = mouseMsg->screenPosition();
140
141 switch (m_clickBehavior) {
142
143 // If the user click outside the window, we have to close
144 // the tooltip window.
145 case ClickBehavior::CloseOnClickInOtherWindow: {
146 Widget* picked = pickFromScreenPos(screenPos);
147 if (!picked || picked->window() != this) {
148 closeWindow(nullptr);
149 }
150 break;
151 }
152
153 case ClickBehavior::CloseOnClickOutsideHotRegion: {
154 // Convert the mousePos from display() coordinates to screen
155 if (!m_hotRegion.contains(screenPos)) {
156 closeWindow(nullptr);
157 }
158 break;
159 }
160 }
161 }
162 break;
163
164 case kMouseMoveMessage:
165 if (m_fixed &&
166 !m_hotRegion.isEmpty() &&
167 manager()->getCapture() == nullptr &&
168 msg->display()) {
169 gfx::Point mousePos = static_cast<MouseMessage*>(msg)->position();
170 gfx::Point screenPos = msg->display()->nativeWindow()->pointToScreen(mousePos);
171
172 // If the mouse is outside the hot-region we have to close the
173 // window.
174 if (!m_hotRegion.contains(screenPos))
175 closeWindow(nullptr);
176 }
177 break;
178
179 }
180
181 return Window::onProcessMessage(msg);
182}
183
184void PopupWindow::onHitTest(HitTestEvent& ev)
185{
186 Window::onHitTest(ev);
187
188 Widget* picked = manager()->pick(ev.point());
189 if (picked) {
190 WidgetType type = picked->type();
191 if (type == kWindowWidget && picked == this) {
192 if (isSizeable() && (ev.hit() == HitTestBorderNW ||
193 ev.hit() == HitTestBorderN ||
194 ev.hit() == HitTestBorderNE ||
195 ev.hit() == HitTestBorderE ||
196 ev.hit() == HitTestBorderSE ||
197 ev.hit() == HitTestBorderS ||
198 ev.hit() == HitTestBorderSW ||
199 ev.hit() == HitTestBorderW)) {
200 // Use the hit value from Window::onHitTest()
201 return;
202 }
203 else {
204 ev.setHit(isMoveable() ? HitTestCaption: HitTestClient);
205 }
206 }
207 else if (type == kBoxWidget ||
208 type == kLabelWidget ||
209 type == kLinkLabelWidget ||
210 type == kGridWidget ||
211 type == kSeparatorWidget) {
212 ev.setHit(isMoveable() ? HitTestCaption: HitTestClient);
213 }
214 }
215}
216
217void PopupWindow::startFilteringMessages()
218{
219 if (!m_filtering) {
220 m_filtering = true;
221
222 Manager* manager = Manager::getDefault();
223 manager->addMessageFilter(kMouseMoveMessage, this);
224 manager->addMessageFilter(kMouseDownMessage, this);
225 manager->addMessageFilter(kKeyDownMessage, this);
226 }
227}
228
229void PopupWindow::stopFilteringMessages()
230{
231 if (m_filtering) {
232 m_filtering = false;
233
234 Manager* manager = Manager::getDefault();
235 manager->removeMessageFilter(kMouseMoveMessage, this);
236 manager->removeMessageFilter(kMouseDownMessage, this);
237 manager->removeMessageFilter(kKeyDownMessage, this);
238 }
239}
240
241void PopupWindow::onMakeFloating()
242{
243 // Do nothing
244}
245
246void PopupWindow::onMakeFixed()
247{
248 // Do nothing
249}
250
251TransparentPopupWindow::TransparentPopupWindow(ClickBehavior clickBehavior)
252 : PopupWindow("", clickBehavior)
253{
254 setTransparent(true);
255 initTheme();
256}
257
258void TransparentPopupWindow::onInitTheme(InitThemeEvent& ev)
259{
260 PopupWindow::onInitTheme(ev);
261 // TODO fix this, if we use alpha=0 (gfx::ColorNone), we get
262 // "window_face" color as background the transparent popup window.
263 //setBgColor(gfx::ColorNone);
264 setBgColor(gfx::rgba(0, 0, 0, 1));
265}
266
267} // namespace ui
268