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