Wiki

Qt/Mac Special Features

by Trenton Schulz
Although Qt is designed to make cross-platform application development easier for our users on all supported platforms, this doesn't mean that we only provide features that are common to all platforms. Where possible, we expose platform-specific functionality in ways that fit in with our cross-platform API, and this is particularly true for Qt/Mac.

On Mac OS X, users expect Qt applications to take advantage of the special window types, styles and events available on that platform. We pass on that functionality to developers in ways that are understandable to developers on other platforms.

New Window Types

Mac OS X provides two types of windows that are unique to the platform: sheets and drawers.

Faded drawers

Drawers are windows that slide out from other windows and are typically used for information that is closely linked with the parent window. You can create a drawer by passing Qt::Drawer as one of the flags in the QWidget constructor. This will just create a window on other platforms. In Qt, you typically get a good user experience if you use this with QDockWidgets.

Faded sheets

Sheets are special dialogs that come down on top of application windows. Each sheet is tied to a window and helps the user to tie a dialog to a certain widget. Typically this is done with "document modal" dialogs. This means that each dialog only interrupts event processing for a single window.

In Qt, you can create a sheet by passing Qt::Sheet as part of your window flags; it has no effect on other platforms.

Thanks to Qt's signals and slots mechanism, it has always been possible to create document modal dialogs with Qt. It just requires a little bit of discipline. Instead of using QDialog::exec() to halt further execution in a function, processing should be done in a slot. Let's modify the MainWindow class from Qt's SDI example to take advantage of this feature.

...
private slots:
    void finishSheet(int sheetValue);
 
private:
    void maybeSave();
    bool reallyQuit;
    QMessageBox *messageBox;
...

We will need an extra slot and an extra boolean variable. It's also useful to reuse our message box. There's no need for the maybeSave() function to return a value since we won't be getting a value right away.

The reallyQuit variable is set to true in setCurrentFile() (shown later), indicating that we can quit when there are no pending changes to be saved. The reallyQuit variable is set to false in the documentWasModified() function if the document is modified:

void MainWindow::documentWasModified()
{
    reallyQuit = !textEdit->document()->isModified();
    setWindowModified(!reallyQuit);
}

We must modify the closeEvent() slightly to make it work in the document modal style:

void MainWindow::closeEvent(QCloseEvent *event)
{
    if (reallyQuit) {
        writeSettings();
        event->accept();
    } else {
        maybeSave();
        event->ignore();
    }
}

Here we check our reallyQuit variable. If it's true, we can safely accept the event and close the window. Otherwise, we ignore the close event and call our new maybeSave() function.

In maybeSave(), we create a warning message box as a sheet if we don't have one already, set its button text to be more helpful, connect its finished() signal to our finishClose() slot:

void MainWindow::maybeSave()
{   
    if (!messageBox) {
        messageBox = new QMessageBox(tr("SDI"),
            tr("The document has been modified.\n"
                "Do you want to save your changes?"),
            QMessageBox::Warning,
            QMessageBox::Yes | QMessageBox::Default,
            QMessageBox::No,
            QMessageBox::Cancel | QMessageBox::Escape,
            this, Qt::Sheet);
 
        messageBox->setButtonText(QMessageBox::Yes,
            isUntitled ? tr("Save...") : tr("Save"));
        messageBox->setButtonText(QMessageBox::No,
            tr("Don't Save"));
 
        connect(messageBox, SIGNAL(finished(int)),
                this, SLOT(finishClose(int)));
    }
    messageBox->show();  
}

Finally, we show the message box and continue on our way. The message box will block any further access on the window until the user decides what to do. At that point, the finished() signal will be emitted with information about the button that was pressed.

In the finishClose() slot, we can finish closing the window if the user selects "Save..." and saves the document, or selects "Don't Save" instead.

void MainWindow::finishClose(int sheetValue)
{
    switch (sheetValue) {
    case QMessageBox::Yes:
        reallyQuit = save();
        if (!reallyQuit) return;
        break;
    case QMessageBox::No:
        reallyQuit = true;
        break;
    case QMessageBox::Cancel:
    default:
        return;
    }
    close();
}

If the cancel button was pressed, we don't want to do anything, so we leave the slot immediately. Otherwise, if the user chose to save, we test if the save succeeds. If it doesn't save successfully, we also leave immediately. If we succeed or the user doesn't want to save, we call close() again, which will finally close the window.

New Looks

Qt 4's support for the Mac's native interfaces makes it possible to give applications even more of a native look and feel.

Brushed Metal

Along with the standard windows, you can also use windows with a brushed metal appearance by adding the WA_MacMetalStyle attribute to your window.

New looks

Custom Dock Menus

A dock menu

It has always been possible to change the icon on the dock by using QApplication::setIcon(). It is now also possible to set a QMenu for the dock icon as well. This is available through a C++ function, but is not in a header, so you need to extern it yourself.

QMenu *menu = new QMenu;
// Add actions to the menu
// Connect them to slots
...
extern void qt_mac_set_dock_menu(QMenu *);
qt_mac_set_dock_menu(menu);
Under the Hood

Qt 4 uses the latest technologies from Apple's Human Interface Toolbox. This means that each QWidget is an HIView and we use Quartz2D for the underlying paint engine. It also means that we get composited windows "for free". However, it also means that you can't paint on widgets outside of a paint event.

This is different from Qt 3 on Mac OS X, where all the widgets were simply regions in the window. This also makes it possible to place other HIViews inside a Qt window, or Qt widgets inside other Carbon windows.

New Events

Qt 4 also introduces some new events that are currently only sent on Mac OS X, but help keep code platform-independent.

QFileOpenEvent

Avid readers of Qt Quarterly will remember an article in issue 12 about dealing with the Apple Open Event. This has been simplified greatly with the introduction of the FileOpen event type. Now, all that's needed is to subclass QApplication, reimplement event(), and handle the FileOpen event. Here's a sample implementation:

class AwesomeApplication : public QApplication
{
    Q_OBJECT
public:
    ...
protected:
    bool event(QEvent *);
    ...
private:
    void loadFile(const QString &fileName);
};
 
bool AwesomeApplication::event(QEvent *event)
{
    switch (event->type()) {
    case QEvent::FileOpen:
        loadFile(static_cast<QFileOpenEvent *>(
                 event)->file());        
        return true;
    default:
        return QApplication::event(event);
    }
}

You still have to create a custom Info.plist that registers your application with the proper file types. The information is included back in issue 12.

QIconDragEvent

You can set the icon on the window with QWidget::setIcon(). On Mac OS X, this is called a proxy icon because it can also act as a "proxy" for the file you are working on. This means you can drag the icon and use it as a file reference. You can use Command+Click on the icon to reveal the file's location.

The QIconDragEvent is sent whenever someone clicks on the proxy icon. With a little bit of code, you can create your own proxy icon. We'll transform Qt's Application example to work with this.

class MainWindow : public QMainWindow
{
    Q_OBJECT
    ...
protected:
    bool event(QEvent *event);
    ...
private slots:
    void openAt(QAction *where);
    ...
private:
    QIcon fileIcon;
    ...
};

We have added some new functions to our MainWindow class. The obvious one is the event() function. But we will need the openAt() helper slot later. We keep a QIcon around for the file icon which we obtain in the constructor by calling the style's standardIcon() function to get the icon from QStyle's repository:

fileIcon = style()->standardIcon(QStyle::SP_FileIcon, 0, this);

The real action happens in our event() function:

bool MainWindow::event(QEvent *event)
{
    if (!isActiveWindow())
        return QMainWindow::event(event);

First, we only really care about processing the IconDrag event if our window is active (if it isn't, we just call our super class's function). We check the type and if it's the IconDrag event, we accept the event and check the current keyboard modifiers:

    switch (event->type()) {
      case QEvent::IconDrag: {
        event->accept();
        Qt::KeyboardModifiers currentModifiers =
            qApp->keyboardModifiers();
 
        if (currentModifiers == Qt::NoModifier) {
            QDrag *drag = new QDrag(this);
            QMimeData *data = new QMimeData();
            data->setUrls(QUrl::fromLocalFile(curFile));
            drag->setMimeData(data);

If we have no modifiers then we want to drag the file. So, we first create a QDrag object and create some QMimeData that contains the QUrl of the file that we are working on.

 
            QPixmap cursorPixmap = style()->standardPixmap(QStyle::SP_FileIcon, 0, this); 
            drag->setPixmap(cursorPixmap);
 
            QPoint hotspot(cursorPixmap.width() - 5, 5);
            drag->setHotSpot(hotspot);
 
            drag->start(Qt::LinkAction | Qt::CopyAction);

Since we want to create the illusion of dragging an icon, we use the icon itself as the drag's pixmap, set the hotspot of the cursor to be at the top-right corner of the pixmap. We then start the drag, allowing only copy and link actions.

When users Command+Click on the icon, we want to show a popup menu showing where the file is located. The Command modifier is represented by the generic Qt::ControlModifier flag:

        } else if (currentModifiers==Qt::ControlModifier) {
            QMenu menu(this);
            connect(&menu, SIGNAL(triggered(QAction *)));
 
            QFileInfo info(curFile);
            QAction *action = menu.addAction(info.fileName());
            action->setIcon(fileIcon);

We start by creating a menu and connecting its triggered signal to the openAt() slot. We then split up our path with the file name and create an action for each part of the path.

            QStringList folders = info.absolutePath().split('/');
            QStringListIterator it(folders);
 
            it.toBack();
            while (it.hasPrevious()) {
                QString string = it.previous();
                QIcon icon;
 
                if (!string.isEmpty()) {
                    icon = style()->standardIcon(QStyle::SP_DirClosedIcon, 0, this);
                } else { // At the root
                    string = "/";
                    icon = style()->standardIcon(QStyle::SP_DriveHDIcon, 0, this);
                }
                action = menu.addAction(string);
                action->setIcon(icon);
            }

We also make sure we pick an appropriate icon for that part of the path.

            QPoint pos(QCursor::pos().x() - 20, frameGeometry().y());
            menu.exec(pos);

Finally, we place the menu in a nice place and call exec() on it.

We ignore icon drags using other combinations of modifiers; even so, we have handled the event so we return true:

        } else {
            event->ignore();
        }
        return true;
      }
      default:
        return QMainWindow::event(event);
    }
}

Now we need to take a look at the openAt() slot:

void MainWindow::openAt(QAction *action)
{
    QString path = curFile.left(
        curFile.indexOf(action->text())) + action->text();
    if (path == curFile)
        return;
    QProcess process;
    process.start("/usr/bin/open", QStringList() << path, QIODevice::ReadOnly);
    process.waitForFinished();
}

This might not be the most comprehensible code that ever was written, but it serves our purpose. We first take the text of the QAction passed in and try to construct a path from it. If the path is the current file, there is no need to do anything. Otherwise we create a QProcess, call /usr/bin/open on the path, and wait for the process to finish. The open command will query Launch Services for what it should do with the path and send the appropriate open event to the correct program. For directories, Finder will open a window at that location. While there is certainly more than one way to deal with paths, this one requires the least amount of effort from us.

A proxy icon

This completes our implementation to deal with the proxy icon, but there's a bit more work we need to do to add the finishing touches that give rich feedback to our users. Let's take a look at what other things we should do:

void MainWindow::setCurrentFile(const QString &fileName)
{
    curFile = fileName;
    textEdit->document()->setModified(false);
    setWindowModified(false);
 
    QString shownName;
    QIcon icon;
    if (curFile.isEmpty()) {
        shownName = "untitled.txt";
    } else {
        shownName = strippedName(curFile);
        icon = fileIcon;
    }
 
    setWindowTitle(tr("%1[*] - %2").arg(shownName).arg(tr("Application")));
    setWindowIcon(icon);
}

The first thing we need to do is make sure that we don't show an icon when we start with a brand new document. The main reason is that there is no file to save yet, so it is impossible to drag or show where it is in the file system. If we are opening a file that does exist, we definitely want the icon, so we set it to the file icon we created at start up.

If the document is modified, we of course mark it in the title bar with setWindowModified(), but we can also darken the icon as well (with a function not shown here) to make it a little bit more obvious.

void MainWindow::documentWasModified()
{
    bool modified = textEdit->document()->isModified();
 
    if (!curFile.isEmpty()) {
        if (!modified) {
            setWindowIcon(fileIcon);
        } else {
            static QIcon darkIcon;
 
            if (darkIcon.isNull())
                darkIcon = QIcon(darkenPixmap(fileIcon.pixmap(16, 16)));
            setWindowIcon(darkIcon);
        }
    }
 
    setWindowModified(modified);
}

Modifying a proxy icon

Here we simply create a "darkened" icon from our normal one by converting the pixmap to an image and darkening each pixel. We keep a static QIcon around so we only have to do this darkening once. When the file is no longer modified, as a result of either saving or undoing, we simply reset our file icon.

And with that we have enabled our application to make use of proxy icons.

Conclusions

This was only a whirlwind tour of various features that were introduced in Qt/Mac for Qt 4. More information about Mac-specific issues, such as configuration and deployment (including qmake's support for Universal Binaries), is available in the Qt documentation and on the Trolltech website.


Copyright © 2006 Trolltech Trademarks