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