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 while (true) { 243 switch (alignment.vertical) { 244 case B_ALIGN_TOP: 245 location.y = where.y - size.height - offset.y; 246 if (location.y < screenFrame.top) { 247 alignment.vertical = B_ALIGN_BOTTOM; 248 continue; 249 } 250 break; 251 252 case B_ALIGN_MIDDLE: 253 location.y -= size.height / 2 - offset.y; 254 if (location.y < screenFrame.top) 255 location.y = screenFrame.top; 256 else if (location.y + size.height > screenFrame.bottom) 257 location.y = screenFrame.bottom - size.height; 258 break; 259 260 default: 261 location.y = where.y + offset.y; 262 if (location.y + size.height > screenFrame.bottom) { 263 alignment.vertical = B_ALIGN_TOP; 264 continue; 265 } 266 break; 267 } 268 break; 269 } 270 271 where = location; 272 273 // Cut off any out-of-screen areas 274 275 if (screenFrame.left > where.x) { 276 size.width -= where.x - screenFrame.left; 277 where.x = screenFrame.left; 278 } else if (screenFrame.right < where.x + size.width) 279 size.width = screenFrame.right - where.x; 280 281 if (screenFrame.top > where.y) { 282 size.height -= where.y - screenFrame.top; 283 where.y = screenFrame.top; 284 } else if (screenFrame.bottom < where.y + size.height) 285 size.height -= screenFrame.bottom - where.y; 286 287 // Change window frame 288 289 Window()->ResizeTo(size.width, size.height); 290 Window()->MoveTo(where); 291 } 292 293 294 // #pragma mark - 295 296 297 ToolTipWindow::ToolTipWindow(BToolTip* tip, BPoint where, void* owner) 298 : 299 BWindow(BRect(0, 0, 250, 10).OffsetBySelf(where), "tool tip", 300 B_BORDERED_WINDOW_LOOK, kMenuWindowFeel, 301 B_NOT_ZOOMABLE | B_NOT_MINIMIZABLE | B_AUTO_UPDATE_SIZE_LIMITS 302 | B_AVOID_FRONT | B_AVOID_FOCUS), 303 fOwner(owner) 304 { 305 SetLayout(new BGroupLayout(B_VERTICAL)); 306 307 BToolTipManager* manager = BToolTipManager::Manager(); 308 ToolTipView* view = new ToolTipView(tip); 309 310 manager->Lock(); 311 AddChild(view); 312 manager->Unlock(); 313 314 // figure out size and location 315 316 view->ResetWindowFrame(where); 317 } 318 319 320 void 321 ToolTipWindow::MessageReceived(BMessage* message) 322 { 323 ToolTipView* view = static_cast<ToolTipView*>(ChildAt(0)); 324 325 switch (message->what) { 326 case kMsgHideToolTip: 327 view->HideTip(); 328 break; 329 330 case kMsgCurrentToolTip: 331 { 332 BToolTip* tip = view->Tip(); 333 334 BMessage reply(B_REPLY); 335 reply.AddPointer("current", tip); 336 reply.AddPointer("owner", fOwner); 337 338 if (message->SendReply(&reply) == B_OK) 339 tip->AcquireReference(); 340 break; 341 } 342 343 case kMsgShowToolTip: 344 view->ShowTip(); 345 break; 346 347 case kMsgCloseToolTip: 348 if (view->IsTipHidden()) 349 Quit(); 350 break; 351 352 default: 353 BWindow::MessageReceived(message); 354 } 355 } 356 357 358 } // namespace BPrivate 359 360 361 // #pragma mark - 362 363 364 /*static*/ BToolTipManager* 365 BToolTipManager::Manager() 366 { 367 // Note: The check is not necessary; it's just faster than always calling 368 // pthread_once(). It requires reading/writing of pointers to be atomic 369 // on the architecture. 370 if (sDefaultInstance == NULL) 371 pthread_once(&sManagerInitOnce, &_InitSingleton); 372 373 return sDefaultInstance; 374 } 375 376 377 void 378 BToolTipManager::ShowTip(BToolTip* tip, BPoint where, void* owner) 379 { 380 BToolTip* current = NULL; 381 void* currentOwner = NULL; 382 BMessage reply; 383 if (fWindow.SendMessage(kMsgCurrentToolTip, &reply) == B_OK) { 384 reply.FindPointer("current", (void**)¤t); 385 reply.FindPointer("owner", ¤tOwner); 386 } 387 388 // Release reference from the message 389 if (current != NULL) 390 current->ReleaseReference(); 391 392 if (current == tip || currentOwner == owner) { 393 fWindow.SendMessage(kMsgShowToolTip); 394 return; 395 } 396 397 fWindow.SendMessage(kMsgHideToolTip); 398 399 if (tip != NULL) { 400 BWindow* window = new BPrivate::ToolTipWindow(tip, where, owner); 401 window->Show(); 402 403 fWindow = BMessenger(window); 404 } 405 } 406 407 408 void 409 BToolTipManager::HideTip() 410 { 411 fWindow.SendMessage(kMsgHideToolTip); 412 } 413 414 415 void 416 BToolTipManager::SetShowDelay(bigtime_t time) 417 { 418 // between 10ms and 3s 419 if (time < 10000) 420 time = 10000; 421 else if (time > 3000000) 422 time = 3000000; 423 424 fShowDelay = time; 425 } 426 427 428 bigtime_t 429 BToolTipManager::ShowDelay() const 430 { 431 return fShowDelay; 432 } 433 434 435 void 436 BToolTipManager::SetHideDelay(bigtime_t time) 437 { 438 // between 0 and 0.5s 439 if (time < 0) 440 time = 0; 441 else if (time > 500000) 442 time = 500000; 443 444 fHideDelay = time; 445 } 446 447 448 bigtime_t 449 BToolTipManager::HideDelay() const 450 { 451 return fHideDelay; 452 } 453 454 455 BToolTipManager::BToolTipManager() 456 : 457 fLock("tool tip manager"), 458 fShowDelay(750000), 459 fHideDelay(50000) 460 { 461 } 462 463 464 BToolTipManager::~BToolTipManager() 465 { 466 } 467 468 469 /*static*/ void 470 BToolTipManager::_InitSingleton() 471 { 472 sDefaultInstance = new BToolTipManager(); 473 } 474