| 1 | // Aseprite UI Library |
| 2 | // Copyright (C) 2018-2022 Igara Studio S.A. |
| 3 | // Copyright (C) 2001-2018 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/tooltips.h" |
| 13 | |
| 14 | #include "gfx/size.h" |
| 15 | #include "ui/fit_bounds.h" |
| 16 | #include "ui/graphics.h" |
| 17 | #include "ui/intern.h" |
| 18 | #include "ui/manager.h" |
| 19 | #include "ui/message.h" |
| 20 | #include "ui/paint_event.h" |
| 21 | #include "ui/scale.h" |
| 22 | #include "ui/size_hint_event.h" |
| 23 | #include "ui/system.h" |
| 24 | #include "ui/textbox.h" |
| 25 | #include "ui/theme.h" |
| 26 | |
| 27 | #include <string> |
| 28 | |
| 29 | static const int kTooltipDelayMsecs = 300; |
| 30 | |
| 31 | namespace ui { |
| 32 | |
| 33 | using namespace gfx; |
| 34 | |
| 35 | TooltipManager::TooltipManager() |
| 36 | : Widget(kGenericWidget) |
| 37 | { |
| 38 | Manager* manager = Manager::getDefault(); |
| 39 | manager->addMessageFilter(kMouseEnterMessage, this); |
| 40 | manager->addMessageFilter(kKeyDownMessage, this); |
| 41 | manager->addMessageFilter(kMouseDownMessage, this); |
| 42 | manager->addMessageFilter(kMouseLeaveMessage, this); |
| 43 | |
| 44 | setVisible(false); |
| 45 | } |
| 46 | |
| 47 | TooltipManager::~TooltipManager() |
| 48 | { |
| 49 | Manager* manager = Manager::getDefault(); |
| 50 | manager->removeMessageFilterFor(this); |
| 51 | } |
| 52 | |
| 53 | void TooltipManager::addTooltipFor(Widget* widget, const std::string& text, int arrowAlign) |
| 54 | { |
| 55 | ASSERT(!widget->hasFlags(IGNORE_MOUSE)); |
| 56 | |
| 57 | m_tips[widget] = TipInfo(text, arrowAlign); |
| 58 | } |
| 59 | |
| 60 | void TooltipManager::removeTooltipFor(Widget* widget) |
| 61 | { |
| 62 | auto it = m_tips.find(widget); |
| 63 | if (it != m_tips.end()) |
| 64 | m_tips.erase(it); |
| 65 | } |
| 66 | |
| 67 | bool TooltipManager::onProcessMessage(Message* msg) |
| 68 | { |
| 69 | switch (msg->type()) { |
| 70 | |
| 71 | case kMouseEnterMessage: { |
| 72 | // Tooltips are only for widgets that can directly get the mouse |
| 73 | // (get the kMouseEnterMessage directly). |
| 74 | if (Widget* widget = msg->recipient()) { |
| 75 | Tips::iterator it = m_tips.find(widget); |
| 76 | if (it != m_tips.end()) { |
| 77 | m_target.widget = it->first; |
| 78 | m_target.tipInfo = it->second; |
| 79 | |
| 80 | if (m_timer == nullptr) { |
| 81 | m_timer.reset(new Timer(kTooltipDelayMsecs, this)); |
| 82 | m_timer->Tick.connect(&TooltipManager::onTick, this); |
| 83 | } |
| 84 | |
| 85 | m_timer->start(); |
| 86 | } |
| 87 | } |
| 88 | break; |
| 89 | } |
| 90 | |
| 91 | case kKeyDownMessage: |
| 92 | case kMouseDownMessage: |
| 93 | case kMouseLeaveMessage: |
| 94 | if (m_tipWindow) { |
| 95 | m_tipWindow->closeWindow(nullptr); |
| 96 | m_tipWindow.reset(); |
| 97 | } |
| 98 | |
| 99 | if (m_timer) |
| 100 | m_timer->stop(); |
| 101 | |
| 102 | return false; |
| 103 | } |
| 104 | |
| 105 | return Widget::onProcessMessage(msg); |
| 106 | } |
| 107 | |
| 108 | void TooltipManager::onInitTheme(InitThemeEvent& ev) |
| 109 | { |
| 110 | Widget::onInitTheme(ev); |
| 111 | if (m_tipWindow) |
| 112 | m_tipWindow->initTheme(); |
| 113 | } |
| 114 | |
| 115 | void TooltipManager::onTick() |
| 116 | { |
| 117 | if (!m_tipWindow) { |
| 118 | m_tipWindow.reset(new TipWindow(m_target.tipInfo.text)); |
| 119 | |
| 120 | int arrowAlign = m_target.tipInfo.arrowAlign; |
| 121 | gfx::Rect target = m_target.widget->bounds(); |
| 122 | if (!arrowAlign) |
| 123 | target.setOrigin(m_target.widget->mousePosInDisplay()+12*guiscale()); |
| 124 | |
| 125 | ui::Display* targetDisplay = m_target.widget->display(); |
| 126 | |
| 127 | if (m_tipWindow->pointAt(arrowAlign, |
| 128 | target, |
| 129 | targetDisplay)) { |
| 130 | m_tipWindow->openWindow(); |
| 131 | m_tipWindow->adjustTargetFrom(targetDisplay); |
| 132 | } |
| 133 | else { |
| 134 | // No enough room for the tooltip |
| 135 | m_tipWindow.reset(); |
| 136 | m_timer->stop(); |
| 137 | } |
| 138 | } |
| 139 | m_timer->stop(); |
| 140 | } |
| 141 | |
| 142 | // TipWindow |
| 143 | |
| 144 | TipWindow::TipWindow(const std::string& text) |
| 145 | // Put an empty string in the ctor so the window label isn't build |
| 146 | : PopupWindow("" , ClickBehavior::CloseOnClickInOtherWindow) |
| 147 | , m_arrowStyle(nullptr) |
| 148 | , m_arrowAlign(0) |
| 149 | , m_closeOnKeyDown(true) |
| 150 | , m_textBox(new TextBox("" , LEFT | TOP)) |
| 151 | { |
| 152 | setTransparent(true); |
| 153 | |
| 154 | // Here we build our own custimized label for the window |
| 155 | // (a text box). |
| 156 | m_textBox->setVisible(false); |
| 157 | addChild(m_textBox); |
| 158 | setText(text); |
| 159 | |
| 160 | makeFixed(); |
| 161 | initTheme(); |
| 162 | } |
| 163 | |
| 164 | void TipWindow::setCloseOnKeyDown(bool state) |
| 165 | { |
| 166 | m_closeOnKeyDown = state; |
| 167 | } |
| 168 | |
| 169 | bool TipWindow::pointAt(int arrowAlign, |
| 170 | const gfx::Rect& target, |
| 171 | const ui::Display* display) |
| 172 | { |
| 173 | // TODO merge this code with the new ui::fit_bounds() algorithm |
| 174 | |
| 175 | m_target = target; |
| 176 | m_arrowAlign = arrowAlign; |
| 177 | |
| 178 | remapWindow(); |
| 179 | |
| 180 | int x = target.x; |
| 181 | int y = target.y; |
| 182 | int w = bounds().w; |
| 183 | int h = bounds().h; |
| 184 | |
| 185 | os::Window* nativeParentWindow = display->nativeWindow(); |
| 186 | |
| 187 | int trycount = 0; |
| 188 | for (; trycount < 4; ++trycount) { |
| 189 | switch (arrowAlign) { |
| 190 | case TOP | LEFT: |
| 191 | x = m_target.x + m_target.w; |
| 192 | y = m_target.y + m_target.h; |
| 193 | break; |
| 194 | case TOP | RIGHT: |
| 195 | x = m_target.x - w; |
| 196 | y = m_target.y + m_target.h; |
| 197 | break; |
| 198 | case BOTTOM | LEFT: |
| 199 | x = m_target.x + m_target.w; |
| 200 | y = m_target.y - h; |
| 201 | break; |
| 202 | case BOTTOM | RIGHT: |
| 203 | x = m_target.x - w; |
| 204 | y = m_target.y - h; |
| 205 | break; |
| 206 | case TOP: |
| 207 | x = m_target.x + m_target.w/2 - w/2; |
| 208 | y = m_target.y + m_target.h; |
| 209 | break; |
| 210 | case BOTTOM: |
| 211 | x = m_target.x + m_target.w/2 - w/2; |
| 212 | y = m_target.y - h; |
| 213 | break; |
| 214 | case LEFT: |
| 215 | x = m_target.x + m_target.w; |
| 216 | y = m_target.y + m_target.h/2 - h/2; |
| 217 | break; |
| 218 | case RIGHT: |
| 219 | x = m_target.x - w; |
| 220 | y = m_target.y + m_target.h/2 - h/2; |
| 221 | break; |
| 222 | } |
| 223 | |
| 224 | if (get_multiple_displays()) { |
| 225 | const gfx::Rect waBounds = nativeParentWindow->screen()->workarea(); |
| 226 | gfx::Point pt = nativeParentWindow->pointToScreen(gfx::Point(x, y)); |
| 227 | pt.x = std::clamp(pt.x, waBounds.x, waBounds.x2()-w); |
| 228 | pt.y = std::clamp(pt.y, waBounds.y, waBounds.y2()-h); |
| 229 | pt = nativeParentWindow->pointFromScreen(pt); |
| 230 | x = pt.x; |
| 231 | y = pt.y; |
| 232 | } |
| 233 | else { |
| 234 | const gfx::Rect displayBounds = display->bounds(); |
| 235 | x = std::clamp(x, displayBounds.x, displayBounds.x2()-w); |
| 236 | y = std::clamp(y, displayBounds.y, displayBounds.y2()-h); |
| 237 | } |
| 238 | |
| 239 | if (m_target.intersects(gfx::Rect(x, y, w, h))) { |
| 240 | switch (trycount) { |
| 241 | case 0: |
| 242 | case 2: |
| 243 | // Switch position |
| 244 | if (arrowAlign & (TOP | BOTTOM)) arrowAlign ^= TOP | BOTTOM; |
| 245 | if (arrowAlign & (LEFT | RIGHT)) arrowAlign ^= LEFT | RIGHT; |
| 246 | break; |
| 247 | case 1: |
| 248 | // Rotate positions |
| 249 | if (arrowAlign & (TOP | LEFT)) arrowAlign ^= TOP | LEFT; |
| 250 | if (arrowAlign & (BOTTOM | RIGHT)) arrowAlign ^= BOTTOM | RIGHT; |
| 251 | break; |
| 252 | } |
| 253 | } |
| 254 | else { |
| 255 | m_arrowAlign = arrowAlign; |
| 256 | ui::fit_bounds(display, this, gfx::Rect(x, y, w, h)); |
| 257 | break; |
| 258 | } |
| 259 | } |
| 260 | |
| 261 | return (trycount < 4); |
| 262 | } |
| 263 | |
| 264 | void TipWindow::adjustTargetFrom(const ui::Display* targetDisplay) |
| 265 | { |
| 266 | // Convert the target relative to this window coordinates |
| 267 | if (get_multiple_displays()) { |
| 268 | gfx::Point pt = m_target.origin(); |
| 269 | pt = targetDisplay->nativeWindow()->pointToScreen(pt); |
| 270 | pt = display()->nativeWindow()->pointFromScreen(pt); |
| 271 | m_target.setOrigin(pt); |
| 272 | } |
| 273 | else { |
| 274 | m_target.offset(-bounds().origin()); |
| 275 | } |
| 276 | } |
| 277 | |
| 278 | bool TipWindow::onProcessMessage(Message* msg) |
| 279 | { |
| 280 | switch (msg->type()) { |
| 281 | |
| 282 | case kKeyDownMessage: |
| 283 | if (m_closeOnKeyDown && |
| 284 | static_cast<KeyMessage*>(msg)->scancode() < kKeyFirstModifierScancode) |
| 285 | closeWindow(nullptr); |
| 286 | break; |
| 287 | |
| 288 | } |
| 289 | |
| 290 | return PopupWindow::onProcessMessage(msg); |
| 291 | } |
| 292 | |
| 293 | void TipWindow::onPaint(PaintEvent& ev) |
| 294 | { |
| 295 | theme()->paintTooltip( |
| 296 | ev.graphics(), this, style(), arrowStyle(), |
| 297 | clientBounds(), arrowAlign(), |
| 298 | target()); |
| 299 | } |
| 300 | |
| 301 | void TipWindow::onBuildTitleLabel() |
| 302 | { |
| 303 | if (!text().empty()) { |
| 304 | m_textBox->setVisible(true); |
| 305 | m_textBox->setText(text()); |
| 306 | } |
| 307 | else |
| 308 | m_textBox->setVisible(false); |
| 309 | } |
| 310 | |
| 311 | } // namespace ui |
| 312 | |