My blog has been moved to ariya.ofilabs.com.

Wednesday, April 18, 2007

Tab bar with RoundedNorth for tabbed dock widgets

Warning: don't try this in real world's application!

Update: I found a trick to reduce the flicker, just read further on.

One advantage of dock widgets (using QDockWidget) is that the docks can be "stacked", either programatically using tabifyDockWidget or when the user explicitly places one dock on top of another. Example is shown below (for future version of SpeedCrunch, more about this in another blog post). There are two dock widgets: History and Functions, and at the moment they are stacked.

However, apparently the tab bar (QTabBar) which is used to choose the dock is always placed at the bottom. Or, using the Qt's terminology, it has the shape of QTabBar::RoundedSouth. Since there are other possibilities for tab bar's shape, e.g. RoundedNorth, would it be possible to make something like this, where the tab is place at the top?

Unfortunately until Qt 4.2 this is not possible yet (issue 146772), although according to issue 145880, "vertical tab bar layout" will be possible in Qt 4.3.

Just for fun, I found a very hackish way to make RoundedNorth for the tab bar (hence, the screenshot above). The trick is to find the tab bar using run-time introspection feature of Qt and then change the geometry manually. If I know in advance that there will be only one tab bar in my main window and there are only two dock widgets, this can be accomplished with a private slot like this:

void MainWindow::hackTabbedDocks()
{
 QDockWidget* topDock = d->historyDock;
 if( topDock->height() == 0)
   topDock = d->functionsDock;

 QList<QTabBar *> allTabs = findChildren<QTabBar *>();
 for(int ii = 0; ii < allTabs.size(); ++ii)
 {
   QTabBar* tab = allTabs[ii];
   if(tab->parentWidget() == this)
   {
     if(tab->geometry().top() > topDock->geometry().top())
     {
       tab->setShape( QTabBar::RoundedNorth );
       int h = tab->geometry().height();
       tab->move(tab->geometry().left(), topDock->geometry().top());
       topDock->move(topDock->geometry().left(), topDock->geometry().top()+h);
     }
     break;
   }
 }
}

I found out, I always need to call this slot twice so that it can work. To simplify, there two other slots which manage it:

void MainWindow::handleTabChange()
{
  QTimer::singleShot(0, this, SLOT(hack1()));
}

void MainWindow::hack1()
{
  hackTabbedDocks();
  QTimer::singleShot(100, this, SLOT(hackTabbedDocks()));
}

And I need to bind any signals which indicate that the tab bar has been relayouted, e.g. when a new tab is selected , when it is resized, etc, to handleTabChange slot above. For illustration purpose, let's just do the first. Of course, this can be done only when tab bar already exists. So, time for another silly slot:

void MainWindow::initHack()
{
  QList<QTabBar *> allTabs = findChildren<QTabBar *>();
  for(int ii = 0; ii < allTabs.size(); ++ii)
  {
    QTabBar* tab = allTabs[ii];
    if(tab->parentWidget() == this)
    {
      connect(tab, SIGNAL(currentChanged(int)), this, SLOT(handleTabChange()));
      break;
    }
  }
  handleTabChange();
}

which will be called from MainWindow's constructor by abusing QTimer once more:

  tabifyDockWidget( d->historyDock, d->functionsDock );
  QTimer::singleShot(0, this, SLOT(initHack()));

That's it!

The big disadvantage of this trick is obvious: flicker occurs everytime you do something with the docks, e.g. changing the tab. It will be quite annoying, but all of this is just a hack anyway. So was it fun? Yes. Useful? No.

(I guess the real "solution" is only waiting for the Trolls to implement it)

UPDATE. Here is the trick to reduce the flicker so that it becomes acceptable (even almost not noticeable). I need another private slot and some magic with setUpdatesEnabled as follows:

void MainWindow::handleTabChange()
{
  setUpdatesEnabled(false);
  QTimer::singleShot(0, this, SLOT(hack1()));
}

void MainWindow::hack1()
{
  hackTabbedDocks();
  QTimer::singleShot(0, this, SLOT(hack2()));
}

void MainWindow::hack2()
{
  hackTabbedDocks();
  setUpdatesEnabled(true);
}