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