1 /* 2 * Copyright 2009-2012, Axel Dörfler, axeld@pinc-software.de. 3 * Copyright 2009, Stephan Aßmus <superstippi@gmx.de>. 4 * All rights reserved. Distributed under the terms of the MIT License. 5 */ 6 7 8 #include <ToolTipManager.h> 9 #include <ToolTipWindow.h> 10 11 #include <pthread.h> 12 13 #include <Autolock.h> 14 #include <LayoutBuilder.h> 15 #include <MessageRunner.h> 16 #include <Screen.h> 17 18 #include <WindowPrivate.h> 19 #include <ToolTip.h> 20 21 22 static pthread_once_t sManagerInitOnce = PTHREAD_ONCE_INIT; 23 BToolTipManager* BToolTipManager::sDefaultInstance; 24 25 static const uint32 kMsgHideToolTip = 'hide'; 26 static const uint32 kMsgShowToolTip = 'show'; 27 static const uint32 kMsgCurrentToolTip = 'curr'; 28 static const uint32 kMsgCloseToolTip = 'clos'; 29 30 31 namespace BPrivate { 32 33 34 class ToolTipView : public BView { 35 public: 36 ToolTipView(BToolTip* tip); 37 virtual ~ToolTipView(); 38 39 virtual void AttachedToWindow(); 40 virtual void DetachedFromWindow(); 41 42 virtual void FrameResized(float width, float height); 43 virtual void MouseMoved(BPoint where, uint32 transit, 44 const BMessage* dragMessage); 45 virtual void KeyDown(const char* bytes, int32 numBytes); 46 47 void HideTip(); 48 void ShowTip(); 49 50 void ResetWindowFrame(); 51 void ResetWindowFrame(BPoint where); 52 53 BToolTip* Tip() const { return fToolTip; } 54 bool IsTipHidden() const { return fHidden; } 55 56 private: 57 BToolTip* fToolTip; 58 bool fHidden; 59 }; 60 61 62 ToolTipView::ToolTipView(BToolTip* tip) 63 : 64 BView("tool tip", B_WILL_DRAW | B_FRAME_EVENTS), 65 fToolTip(tip), 66 fHidden(false) 67 { 68 fToolTip->AcquireReference(); 69 SetViewUIColor(B_TOOL_TIP_BACKGROUND_COLOR); 70 SetHighUIColor(B_TOOL_TIP_TEXT_COLOR); 71 72 BGroupLayout* layout = new BGroupLayout(B_VERTICAL); 73 layout->SetInsets(5, 5, 5, 5); 74 SetLayout(layout); 75 76 AddChild(fToolTip->View()); 77 } 78 79 80 ToolTipView::~ToolTipView() 81 { 82 fToolTip->ReleaseReference(); 83 } 84 85 86 void 87 ToolTipView::AttachedToWindow() 88 { 89 SetEventMask(B_POINTER_EVENTS | B_KEYBOARD_EVENTS, 0); 90 fToolTip->AttachedToWindow(); 91 } 92 93 94 void 95 ToolTipView::DetachedFromWindow() 96 { 97 BToolTipManager* manager = BToolTipManager::Manager(); 98 manager->Lock(); 99 100 RemoveChild(fToolTip->View()); 101 // don't delete this one! 102 fToolTip->DetachedFromWindow(); 103 104 manager->Unlock(); 105 } 106 107 108 void 109 ToolTipView::FrameResized(float width, float height) 110 { 111 ResetWindowFrame(); 112 } 113 114 115 void 116 ToolTipView::MouseMoved(BPoint where, uint32 transit, 117 const BMessage* dragMessage) 118 { 119 if (fToolTip->IsSticky()) { 120 ResetWindowFrame(ConvertToScreen(where)); 121 } else if (transit == B_ENTERED_VIEW) { 122 // close instantly if the user managed to enter 123 Window()->Quit(); 124 } else { 125 // close with the preferred delay in case the mouse just moved 126 HideTip(); 127 } 128 } 129 130 131 void 132 ToolTipView::KeyDown(const char* bytes, int32 numBytes) 133 { 134 if (!fToolTip->IsSticky()) 135 HideTip(); 136 } 137 138 139 void 140 ToolTipView::HideTip() 141 { 142 if (fHidden) 143 return; 144 145 BMessage quit(kMsgCloseToolTip); 146 BMessageRunner::StartSending(Window(), &quit, 147 BToolTipManager::Manager()->HideDelay(), 1); 148 fHidden = true; 149 } 150 151 152 void 153 ToolTipView::ShowTip() 154 { 155 fHidden = false; 156 } 157 158 159 void 160 ToolTipView::ResetWindowFrame() 161 { 162 BPoint where; 163 GetMouse(&where, NULL, false); 164 165 ResetWindowFrame(ConvertToScreen(where)); 166 } 167 168 169 /*! Tries to find the right frame to show the tool tip in, trying to use the 170 alignment that the tool tip specifies. 171 Makes sure the tool tip can be shown on screen in its entirety, ie. it will 172 resize the window if necessary. 173 */ 174 void 175 ToolTipView::ResetWindowFrame(BPoint where) 176 { 177 if (Window() == NULL) 178 return; 179 180 BSize size = PreferredSize(); 181 182 BScreen screen(Window()); 183 BRect screenFrame = screen.Frame().InsetBySelf(2, 2); 184 BPoint offset = fToolTip->MouseRelativeLocation(); 185 186 // Ensure that the tip can be placed on screen completely 187 188 if (size.width > screenFrame.Width()) 189 size.width = screenFrame.Width(); 190 191 if (size.width > where.x - screenFrame.left 192 && size.width > screenFrame.right - where.x) { 193 // There is no space to put the tip to the left or the right of the 194 // cursor, it can either be below or above it 195 if (size.height > where.y - screenFrame.top 196 && where.y - screenFrame.top > screenFrame.Height() / 2) { 197 size.height = where.y - offset.y - screenFrame.top; 198 } else if (size.height > screenFrame.bottom - where.y 199 && screenFrame.bottom - where.y > screenFrame.Height() / 2) { 200 size.height = screenFrame.bottom - where.y - offset.y; 201 } 202 } 203 204 // Find best alignment, starting with the requested one 205 206 BAlignment alignment = fToolTip->Alignment(); 207 BPoint location = where; 208 bool doesNotFit = false; 209 210 switch (alignment.horizontal) { 211 case B_ALIGN_LEFT: 212 location.x -= size.width + offset.x; 213 if (location.x < screenFrame.left) { 214 location.x = screenFrame.left; 215 doesNotFit = true; 216 } 217 break; 218 case B_ALIGN_CENTER: 219 location.x -= size.width / 2 - offset.x; 220 if (location.x < screenFrame.left) { 221 location.x = screenFrame.left; 222 doesNotFit = true; 223 } else if (location.x + size.width > screenFrame.right) { 224 location.x = screenFrame.right - size.width; 225 doesNotFit = true; 226 } 227 break; 228 229 default: 230 location.x += offset.x; 231 if (location.x + size.width > screenFrame.right) { 232 location.x = screenFrame.right - size.width; 233 doesNotFit = true; 234 } 235 break; 236 } 237 238 if ((doesNotFit && alignment.vertical == B_ALIGN_MIDDLE) 239 || (alignment.vertical == B_ALIGN_MIDDLE 240 && alignment.horizontal == B_ALIGN_CENTER)) 241 alignment.vertical = B_ALIGN_BOTTOM; 242 243 // Adjust the tooltip position in cases where it would be partly out of the 244 // screen frame. Try to fit the tooltip on the requested side of the 245 // cursor, if that fails, try the opposite side, and if that fails again, 246 // give up and leave the tooltip under the mouse cursor. 247 bool firstTry = true; 248 while (true) { 249 switch (alignment.vertical) { 250 case B_ALIGN_TOP: 251 location.y = where.y - size.height - offset.y; 252 if (location.y < screenFrame.top) { 253 alignment.vertical = firstTry ? B_ALIGN_BOTTOM 254 : B_ALIGN_MIDDLE; 255 firstTry = false; 256 continue; 257 } 258 break; 259 260 case B_ALIGN_MIDDLE: 261 location.y -= size.height / 2 - offset.y; 262 if (location.y < screenFrame.top) 263 location.y = screenFrame.top; 264 else if (location.y + size.height > screenFrame.bottom) 265 location.y = screenFrame.bottom - size.height; 266 break; 267 268 default: 269 location.y = where.y + offset.y; 270 if (location.y + size.height > screenFrame.bottom) { 271 alignment.vertical = firstTry ? B_ALIGN_TOP 272 : B_ALIGN_MIDDLE; 273 firstTry = false; 274 continue; 275 } 276 break; 277 } 278 break; 279 } 280 281 where = location; 282 283 // Cut off any out-of-screen areas 284 285 if (screenFrame.left > where.x) { 286 size.width -= where.x - screenFrame.left; 287 where.x = screenFrame.left; 288 } else if (screenFrame.right < where.x + size.width) 289 size.width = screenFrame.right - where.x; 290 291 if (screenFrame.top > where.y) { 292 size.height -= where.y - screenFrame.top; 293 where.y = screenFrame.top; 294 } else if (screenFrame.bottom < where.y + size.height) 295 size.height -= screenFrame.bottom - where.y; 296 297 // Change window frame 298 299 Window()->ResizeTo(size.width, size.height); 300 Window()->MoveTo(where); 301 } 302 303 304 // #pragma mark - 305 306 307 ToolTipWindow::ToolTipWindow(BToolTip* tip, BPoint where, void* owner) 308 : 309 BWindow(BRect(0, 0, 250, 10).OffsetBySelf(where), "tool tip", 310 B_BORDERED_WINDOW_LOOK, kMenuWindowFeel, 311 B_NOT_ZOOMABLE | B_NOT_MINIMIZABLE | B_AUTO_UPDATE_SIZE_LIMITS 312 | B_AVOID_FRONT | B_AVOID_FOCUS), 313 fOwner(owner) 314 { 315 SetLayout(new BGroupLayout(B_VERTICAL)); 316 317 BToolTipManager* manager = BToolTipManager::Manager(); 318 ToolTipView* view = new ToolTipView(tip); 319 320 manager->Lock(); 321 AddChild(view); 322 manager->Unlock(); 323 324 // figure out size and location 325 326 view->ResetWindowFrame(where); 327 } 328 329 330 void 331 ToolTipWindow::MessageReceived(BMessage* message) 332 { 333 ToolTipView* view = static_cast<ToolTipView*>(ChildAt(0)); 334 335 switch (message->what) { 336 case kMsgHideToolTip: 337 view->HideTip(); 338 break; 339 340 case kMsgCurrentToolTip: 341 { 342 BToolTip* tip = view->Tip(); 343 344 BMessage reply(B_REPLY); 345 reply.AddPointer("current", tip); 346 reply.AddPointer("owner", fOwner); 347 348 if (message->SendReply(&reply) == B_OK) 349 tip->AcquireReference(); 350 break; 351 } 352 353 case kMsgShowToolTip: 354 view->ShowTip(); 355 break; 356 357 case kMsgCloseToolTip: 358 if (view->IsTipHidden()) 359 Quit(); 360 break; 361 362 default: 363 BWindow::MessageReceived(message); 364 } 365 } 366 367 368 } // namespace BPrivate 369 370 371 // #pragma mark - 372 373 374 /*static*/ BToolTipManager* 375 BToolTipManager::Manager() 376 { 377 // Note: The check is not necessary; it's just faster than always calling 378 // pthread_once(). It requires reading/writing of pointers to be atomic 379 // on the architecture. 380 if (sDefaultInstance == NULL) 381 pthread_once(&sManagerInitOnce, &_InitSingleton); 382 383 return sDefaultInstance; 384 } 385 386 387 void 388 BToolTipManager::ShowTip(BToolTip* tip, BPoint where, void* owner) 389 { 390 BToolTip* current = NULL; 391 void* currentOwner = NULL; 392 BMessage reply; 393 if (fWindow.SendMessage(kMsgCurrentToolTip, &reply) == B_OK) { 394 reply.FindPointer("current", (void**)¤t); 395 reply.FindPointer("owner", ¤tOwner); 396 } 397 398 // Release reference from the message 399 if (current != NULL) 400 current->ReleaseReference(); 401 402 if (current == tip || currentOwner == owner) { 403 fWindow.SendMessage(kMsgShowToolTip); 404 return; 405 } 406 407 fWindow.SendMessage(kMsgHideToolTip); 408 409 if (tip != NULL) { 410 BWindow* window = new BPrivate::ToolTipWindow(tip, where, owner); 411 window->Show(); 412 413 fWindow = BMessenger(window); 414 } 415 } 416 417 418 void 419 BToolTipManager::HideTip() 420 { 421 fWindow.SendMessage(kMsgHideToolTip); 422 } 423 424 425 void 426 BToolTipManager::SetShowDelay(bigtime_t time) 427 { 428 // between 10ms and 3s 429 if (time < 10000) 430 time = 10000; 431 else if (time > 3000000) 432 time = 3000000; 433 434 fShowDelay = time; 435 } 436 437 438 bigtime_t 439 BToolTipManager::ShowDelay() const 440 { 441 return fShowDelay; 442 } 443 444 445 void 446 BToolTipManager::SetHideDelay(bigtime_t time) 447 { 448 // between 0 and 0.5s 449 if (time < 0) 450 time = 0; 451 else if (time > 500000) 452 time = 500000; 453 454 fHideDelay = time; 455 } 456 457 458 bigtime_t 459 BToolTipManager::HideDelay() const 460 { 461 return fHideDelay; 462 } 463 464 465 BToolTipManager::BToolTipManager() 466 : 467 fLock("tool tip manager"), 468 fShowDelay(750000), 469 fHideDelay(50000) 470 { 471 } 472 473 474 BToolTipManager::~BToolTipManager() 475 { 476 } 477 478 479 /*static*/ void 480 BToolTipManager::_InitSingleton() 481 { 482 sDefaultInstance = new BToolTipManager(); 483 } 484