1// Aseprite UI Library
2// Copyright (C) 2019-2022 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 "ui/int_entry.h"
13
14#include "base/scoped_value.h"
15#include "gfx/rect.h"
16#include "gfx/region.h"
17#include "os/font.h"
18#include "ui/fit_bounds.h"
19#include "ui/manager.h"
20#include "ui/message.h"
21#include "ui/popup_window.h"
22#include "ui/scale.h"
23#include "ui/size_hint_event.h"
24#include "ui/slider.h"
25#include "ui/system.h"
26#include "ui/theme.h"
27
28#include <algorithm>
29#include <cmath>
30
31namespace ui {
32
33using namespace gfx;
34
35IntEntry::IntEntry(int min, int max, SliderDelegate* sliderDelegate)
36 : Entry(int(std::floor(std::log10(double(max))))+1, "")
37 , m_min(min)
38 , m_max(max)
39 , m_slider(m_min, m_max, m_min, sliderDelegate)
40 , m_popupWindow(nullptr)
41 , m_changeFromSlider(false)
42{
43 m_slider.setFocusStop(false); // In this way the IntEntry doesn't lost the focus
44 m_slider.setTransparent(true);
45 m_slider.Change.connect(&IntEntry::onChangeSlider, this);
46 initTheme();
47}
48
49IntEntry::~IntEntry()
50{
51 closePopup();
52}
53
54int IntEntry::getValue() const
55{
56 int value = m_slider.convertTextToValue(text());
57 return std::clamp(value, m_min, m_max);
58}
59
60void IntEntry::setValue(int value)
61{
62 value = std::clamp(value, m_min, m_max);
63
64 setText(m_slider.convertValueToText(value));
65
66 if (m_popupWindow && !m_changeFromSlider)
67 m_slider.setValue(value);
68
69 onValueChange();
70}
71
72bool IntEntry::onProcessMessage(Message* msg)
73{
74 switch (msg->type()) {
75
76 // Reset value if it's out of bounds when focus is lost
77 case kFocusLeaveMessage:
78 setValue(std::clamp(getValue(), m_min, m_max));
79 deselectText();
80 break;
81
82 case kMouseDownMessage:
83 requestFocus();
84 captureMouse();
85
86 openPopup();
87 selectAllText();
88 return true;
89
90 case kMouseMoveMessage:
91 if (hasCapture()) {
92 MouseMessage* mouseMsg = static_cast<MouseMessage*>(msg);
93 Widget* pick = manager()->pickFromScreenPos(
94 display()->nativeWindow()->pointToScreen(mouseMsg->position()));
95 if (pick == &m_slider) {
96 releaseMouse();
97
98 MouseMessage mouseMsg2(kMouseDownMessage,
99 *mouseMsg,
100 mouseMsg->positionForDisplay(pick->display()));
101 mouseMsg2.setDisplay(pick->display());
102 pick->sendMessage(&mouseMsg2);
103 }
104 }
105 break;
106
107 case kMouseWheelMessage:
108 if (isEnabled()) {
109 int oldValue = getValue();
110 int newValue = oldValue
111 + static_cast<MouseMessage*>(msg)->wheelDelta().x
112 - static_cast<MouseMessage*>(msg)->wheelDelta().y;
113 newValue = std::clamp(newValue, m_min, m_max);
114 if (newValue != oldValue) {
115 setValue(newValue);
116 selectAllText();
117 }
118 return true;
119 }
120 break;
121
122 case kKeyDownMessage:
123 if (hasFocus() && !isReadOnly()) {
124 KeyMessage* keymsg = static_cast<KeyMessage*>(msg);
125 int chr = keymsg->unicodeChar();
126 if (chr >= 32 && (chr < '0' || chr > '9')) {
127 // "Eat" all keys that aren't number
128 return true;
129 }
130 // Else we use the default Entry processing function which
131 // will process keys like Left/Right arrows, clipboard
132 // handling, etc.
133 }
134 break;
135 }
136 return Entry::onProcessMessage(msg);
137}
138
139void IntEntry::onInitTheme(InitThemeEvent& ev)
140{
141 Entry::onInitTheme(ev);
142 m_slider.initTheme(); // The slider might not be in the popup window
143 if (m_popupWindow)
144 m_popupWindow->initTheme();
145}
146
147void IntEntry::onSizeHint(SizeHintEvent& ev)
148{
149 int trailing = font()->textLength(getSuffix());
150 trailing = std::max(trailing, 2*theme()->getEntryCaretSize(this).w);
151
152 int min_w = font()->textLength(m_slider.convertValueToText(m_min));
153 int max_w = font()->textLength(m_slider.convertValueToText(m_max)) + trailing;
154
155 int w = std::max(min_w, max_w);
156 int h = textHeight();
157
158 w += border().width();
159 h += border().height();
160
161 ev.setSizeHint(w, h);
162}
163
164void IntEntry::onChange()
165{
166 Entry::onChange();
167 onValueChange();
168}
169
170void IntEntry::onValueChange()
171{
172 // Do nothing
173}
174
175void IntEntry::openPopup()
176{
177 m_slider.setValue(getValue());
178
179 m_popupWindow = std::make_unique<TransparentPopupWindow>(PopupWindow::ClickBehavior::CloseOnClickInOtherWindow);
180 m_popupWindow->setAutoRemap(false);
181 m_popupWindow->addChild(&m_slider);
182 m_popupWindow->Close.connect(&IntEntry::onPopupClose, this);
183
184 fit_bounds(
185 display(),
186 m_popupWindow.get(),
187 gfx::Rect(0, 0, 128*guiscale(), m_popupWindow->sizeHint().h),
188 [this](const gfx::Rect& workarea,
189 gfx::Rect& rc,
190 std::function<gfx::Rect(Widget*)> getWidgetBounds) {
191 Rect entryBounds = getWidgetBounds(this);
192
193 rc.x = entryBounds.x;
194 rc.y = entryBounds.y2();
195
196 if (rc.x2() > workarea.x2())
197 rc.x = rc.x-rc.w+entryBounds.w;
198
199 if (rc.y2() > workarea.y2())
200 rc.y = entryBounds.y-entryBounds.h;
201
202 m_popupWindow->setBounds(rc);
203 });
204
205 Region rgn(m_popupWindow->boundsOnScreen().createUnion(boundsOnScreen()));
206 m_popupWindow->setHotRegion(rgn);
207
208 m_popupWindow->openWindow();
209}
210
211void IntEntry::closePopup()
212{
213 if (m_popupWindow) {
214 removeSlider();
215
216 m_popupWindow->closeWindow(nullptr);
217 m_popupWindow.reset();
218 }
219}
220
221void IntEntry::onChangeSlider()
222{
223 base::ScopedValue<bool> lockFlag(m_changeFromSlider, true, false);
224 setValue(m_slider.getValue());
225 selectAllText();
226}
227
228void IntEntry::onPopupClose(CloseEvent& ev)
229{
230 removeSlider();
231
232 deselectText();
233 releaseFocus();
234}
235
236void IntEntry::removeSlider()
237{
238 if (m_popupWindow &&
239 m_slider.parent() == m_popupWindow.get()) {
240 m_popupWindow->removeChild(&m_slider);
241 }
242}
243
244} // namespace ui
245