| /**************************************************************************** |
| ** |
| ** Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies). |
| ** All rights reserved. |
| ** Contact: Nokia Corporation (qt-info@nokia.com) |
| ** |
| ** This file is part of the QtGui module of the Qt Toolkit. |
| ** |
| ** $QT_BEGIN_LICENSE:LGPL$ |
| ** GNU Lesser General Public License Usage |
| ** This file may be used under the terms of the GNU Lesser General Public |
| ** License version 2.1 as published by the Free Software Foundation and |
| ** appearing in the file LICENSE.LGPL included in the packaging of this |
| ** file. Please review the following information to ensure the GNU Lesser |
| ** General Public License version 2.1 requirements will be met: |
| ** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. |
| ** |
| ** In addition, as a special exception, Nokia gives you certain additional |
| ** rights. These rights are described in the Nokia Qt LGPL Exception |
| ** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. |
| ** |
| ** GNU General Public License Usage |
| ** Alternatively, this file may be used under the terms of the GNU General |
| ** Public License version 3.0 as published by the Free Software Foundation |
| ** and appearing in the file LICENSE.GPL included in the packaging of this |
| ** file. Please review the following information to ensure the GNU General |
| ** Public License version 3.0 requirements will be met: |
| ** http://www.gnu.org/copyleft/gpl.html. |
| ** |
| ** Other Usage |
| ** Alternatively, this file may be used in accordance with the terms and |
| ** conditions contained in a signed written agreement between you and Nokia. |
| ** |
| ** |
| ** |
| ** |
| ** |
| ** $QT_END_LICENSE$ |
| ** |
| ****************************************************************************/ |
| |
| #include "qapplication.h" |
| #include "qevent.h" |
| #include "qbitmap.h" |
| #include "qstyle.h" |
| #include "qmenubar.h" |
| #include "private/qt_s60_p.h" |
| #include "private/qmenu_p.h" |
| #include "private/qaction_p.h" |
| #include "private/qsoftkeymanager_p.h" |
| #include "private/qsoftkeymanager_s60_p.h" |
| #include "private/qobject_p.h" |
| #include <eiksoftkeyimage.h> |
| #include <eikcmbut.h> |
| |
| #ifndef QT_NO_STYLE_S60 |
| #include <qs60style.h> |
| #endif |
| |
| #ifndef QT_NO_SOFTKEYMANAGER |
| QT_BEGIN_NAMESPACE |
| |
| const int S60_COMMAND_START = 6000; |
| const int LSK_POSITION = 0; |
| const int MSK_POSITION = 3; |
| const int RSK_POSITION = 2; |
| |
| QSoftKeyManagerPrivateS60::QSoftKeyManagerPrivateS60() : cbaHasImage(4) // 4 since MSK position index is 3 |
| { |
| cachedCbaIconSize[0] = QSize(0,0); |
| cachedCbaIconSize[1] = QSize(0,0); |
| cachedCbaIconSize[2] = QSize(0,0); |
| cachedCbaIconSize[3] = QSize(0,0); |
| } |
| |
| bool QSoftKeyManagerPrivateS60::skipCbaUpdate() |
| { |
| // Lets not update softkeys if |
| // 1. We don't have application panes, i.e. cba |
| // 2. Our CBA is not active, i.e. S60 native dialog or menu with custom CBA is shown |
| // 2.1. Except if thre is no current CBA at all and WindowSoftkeysRespondHint is set |
| |
| // Note: Cannot use IsDisplayingMenuOrDialog since CBA update can be triggered before |
| // menu/dialog CBA is actually displayed i.e. it is being costructed. |
| CEikButtonGroupContainer *appUiCba = S60->buttonGroupContainer(); |
| if (!appUiCba) |
| return true; |
| // CEikButtonGroupContainer::Current returns 0 if CBA is not visible at all |
| CEikButtonGroupContainer *currentCba = CEikButtonGroupContainer::Current(); |
| // Check if softkey need to be update even they are not visible |
| bool cbaRespondsWhenInvisible = false; |
| QWidget *window = QApplication::activeWindow(); |
| if (window && (window->windowFlags() & Qt::WindowSoftkeysRespondHint)) |
| cbaRespondsWhenInvisible = true; |
| |
| if (QApplication::testAttribute(Qt::AA_S60DontConstructApplicationPanes) |
| || (appUiCba != currentCba && !cbaRespondsWhenInvisible)) { |
| return true; |
| } |
| return false; |
| } |
| |
| void QSoftKeyManagerPrivateS60::ensureCbaVisibilityAndResponsiviness(CEikButtonGroupContainer &cba) |
| { |
| RDrawableWindow *cbaWindow = cba.DrawableWindow(); |
| Q_ASSERT_X(cbaWindow, Q_FUNC_INFO, "Native CBA does not have window!"); |
| // Make sure CBA is visible, i.e. CBA window is on top |
| cbaWindow->SetOrdinalPosition(0); |
| // Qt shares same CBA instance between top-level widgets, |
| // make sure we are not faded by underlying window. |
| cbaWindow->SetFaded(EFalse, RWindowTreeNode::EFadeIncludeChildren); |
| // Modal dialogs capture pointer events, but shared cba instance |
| // shall stay responsive. Raise pointer capture priority to keep |
| // softkeys responsive in modal dialogs |
| cbaWindow->SetPointerCapturePriority(1); |
| } |
| |
| void QSoftKeyManagerPrivateS60::clearSoftkeys(CEikButtonGroupContainer &cba) |
| { |
| #if defined(Q_WS_S60) && !defined(SYMBIAN_VERSION_9_4) && !defined(SYMBIAN_VERSION_9_3) && !defined(SYMBIAN_VERSION_9_2) |
| QT_TRAP_THROWING( |
| //EAknSoftkeyEmpty is used, because using -1 adds softkeys without actions on Symbian3 |
| cba.SetCommandL(0, EAknSoftkeyEmpty, KNullDesC); |
| cba.SetCommandL(2, EAknSoftkeyEmpty, KNullDesC); |
| ); |
| #else |
| QT_TRAP_THROWING( |
| //Using -1 instead of EAknSoftkeyEmpty to avoid flickering. |
| cba.SetCommandL(0, -1, KNullDesC); |
| // TODO: Should we clear also middle SK? |
| cba.SetCommandL(2, -1, KNullDesC); |
| ); |
| #endif |
| realSoftKeyActions.clear(); |
| } |
| |
| QString QSoftKeyManagerPrivateS60::softkeyText(QAction &softkeyAction) |
| { |
| // In S60 softkeys and menu items do not support key accelerators (i.e. |
| // CTRL+X). Therefore, removing the accelerator characters from both softkey |
| // and menu item texts. |
| const int underlineShortCut = QApplication::style()->styleHint(QStyle::SH_UnderlineShortcut); |
| QString iconText = softkeyAction.iconText(); |
| return underlineShortCut ? softkeyAction.text() : iconText; |
| } |
| |
| QAction *QSoftKeyManagerPrivateS60::highestPrioritySoftkey(QAction::SoftKeyRole role) |
| { |
| QAction *ret = NULL; |
| // Priority look up is two level |
| // 1. First widget with softkeys always has highest priority |
| for (int level = 0; !ret; level++) { |
| // 2. Highest priority action within widget |
| QList<QAction*> actions = requestedSoftKeyActions.values(level); |
| if (actions.isEmpty()) |
| break; |
| qSort(actions.begin(), actions.end(), QSoftKeyManagerPrivateS60::actionPriorityMoreThan); |
| foreach (QAction *action, actions) { |
| if (action->softKeyRole() == role) { |
| ret = action; |
| break; |
| } |
| } |
| } |
| return ret; |
| } |
| |
| bool QSoftKeyManagerPrivateS60::actionPriorityMoreThan(const QAction *firstItem, const QAction *secondItem) |
| { |
| return firstItem->priority() > secondItem->priority(); |
| } |
| |
| void QSoftKeyManagerPrivateS60::setNativeSoftkey(CEikButtonGroupContainer &cba, |
| TInt position, TInt command, const TDesC &text) |
| { |
| // Calling SetCommandL causes CBA redraw |
| QT_TRAP_THROWING(cba.SetCommandL(position, command, text)); |
| } |
| |
| QPoint QSoftKeyManagerPrivateS60::softkeyIconPosition(int position, QSize sourceSize, QSize targetSize) |
| { |
| QPoint iconPosition(0,0); |
| |
| // Prior to S60 5.3 icons need to be properly positioned to buttons, but starting with 5.3 |
| // positioning is done on Avkon side. |
| if (QSysInfo::s60Version() < QSysInfo::SV_S60_5_3) { |
| switch (AknLayoutUtils::CbaLocation()) |
| { |
| case AknLayoutUtils::EAknCbaLocationBottom: |
| // RSK must be moved to right, LSK in on correct position by default |
| if (position == RSK_POSITION) |
| iconPosition.setX(targetSize.width() - sourceSize.width()); |
| break; |
| case AknLayoutUtils::EAknCbaLocationRight: |
| case AknLayoutUtils::EAknCbaLocationLeft: |
| // Already in correct position |
| default: |
| break; |
| } |
| |
| // Align horizontally to center |
| iconPosition.setY((targetSize.height() - sourceSize.height()) >> 1); |
| } |
| return iconPosition; |
| } |
| |
| QPixmap QSoftKeyManagerPrivateS60::prepareSoftkeyPixmap(QPixmap src, int position, QSize targetSize) |
| { |
| QPixmap target(targetSize); |
| target.fill(Qt::transparent); |
| QPainter p; |
| p.begin(&target); |
| p.drawPixmap(softkeyIconPosition(position, src.size(), targetSize), src); |
| p.end(); |
| return target; |
| } |
| |
| bool QSoftKeyManagerPrivateS60::isOrientationLandscape() |
| { |
| // Hard to believe that there is no public API in S60 to |
| // get current orientation. This workaround works with currently supported resolutions |
| return S60->screenHeightInPixels < S60->screenWidthInPixels; |
| } |
| |
| QSize QSoftKeyManagerPrivateS60::cbaIconSize(CEikButtonGroupContainer *cba, int position) |
| { |
| int index = position; |
| index += isOrientationLandscape() ? 0 : 1; |
| if(cachedCbaIconSize[index].isNull()) { |
| if (QSysInfo::s60Version() >= QSysInfo::SV_S60_5_3) { |
| // S60 5.3 and later have fixed icon size on softkeys, while the button |
| // itself is bigger, so the automatic check doesn't work. |
| // Use custom pixel metrics to deduce the CBA icon size |
| int iconHeight = 30; |
| int iconWidth = 30; |
| #ifndef QT_NO_STYLE_S60 |
| QS60Style *s60Style = 0; |
| s60Style = qobject_cast<QS60Style *>(QApplication::style()); |
| if (s60Style) { |
| iconWidth = s60Style->pixelMetric((QStyle::PixelMetric)PM_CbaIconWidth); |
| iconHeight = s60Style->pixelMetric((QStyle::PixelMetric)PM_CbaIconHeight); |
| } |
| #endif |
| cachedCbaIconSize[index] = QSize(iconWidth, iconHeight); |
| } else { |
| // Only way I figured out to get CBA icon size without RnD SDK, was |
| // to set some dummy icon to CBA first and then ask CBA button CCoeControl::Size() |
| // The returned value is cached to avoid unnecessary icon setting every time. |
| const bool left = (position == LSK_POSITION); |
| if (position == LSK_POSITION || position == RSK_POSITION) { |
| CEikImage* tmpImage = NULL; |
| QT_TRAP_THROWING(tmpImage = new (ELeave) CEikImage); |
| EikSoftkeyImage::SetImage(cba, *tmpImage, left); // Takes tmpImage ownership |
| int command = S60_COMMAND_START + position; |
| setNativeSoftkey(*cba, position, command, KNullDesC()); |
| cachedCbaIconSize[index] = qt_TSize2QSize(cba->ControlOrNull(command)->Size()); |
| EikSoftkeyImage::SetLabel(cba, left); |
| |
| if (cachedCbaIconSize[index] == QSize(138,72)) { |
| // Hack for S60 5.0 landscape orientation, which return wrong icon size |
| cachedCbaIconSize[index] = QSize(60,60); |
| } |
| } |
| } |
| } |
| |
| return cachedCbaIconSize[index]; |
| } |
| |
| bool QSoftKeyManagerPrivateS60::setSoftkeyImage(CEikButtonGroupContainer *cba, |
| QAction &action, int position) |
| { |
| bool ret = false; |
| |
| const bool left = (position == LSK_POSITION); |
| if(position == LSK_POSITION || position == RSK_POSITION) { |
| QIcon icon = action.icon(); |
| if (!icon.isNull()) { |
| // Get size of CBA icon area based on button position and orientation |
| QSize requiredIconSize = cbaIconSize(cba, position); |
| // Get pixmap out of icon based on preferred size, the aspect ratio is kept |
| QPixmap pmWihtAspectRatio = icon.pixmap(requiredIconSize); |
| // Native softkeys require that pixmap size is exactly the same as requiredIconSize |
| // prepareSoftkeyPixmap creates a new pixmap with requiredIconSize and blits the 'pmWihtAspectRatio' |
| // to correct location of it |
| QPixmap softkeyPixmap = prepareSoftkeyPixmap(pmWihtAspectRatio, position, requiredIconSize); |
| |
| QPixmap softkeyAlpha = softkeyPixmap.alphaChannel(); |
| // Alpha channel in 5.1 and older devices need to be inverted |
| // TODO: Switch to use toSymbianCFbsBitmap with invert when available |
| if(QSysInfo::s60Version() <= QSysInfo::SV_S60_5_1) { |
| QImage alphaImage = softkeyAlpha.toImage(); |
| alphaImage.invertPixels(); |
| softkeyAlpha = QPixmap::fromImage(alphaImage); |
| } |
| |
| CFbsBitmap* nBitmap = softkeyPixmap.toSymbianCFbsBitmap(); |
| CFbsBitmap* nMask = softkeyAlpha.toSymbianCFbsBitmap(); |
| |
| CEikImage* myimage = new (ELeave) CEikImage; |
| myimage->SetPicture( nBitmap, nMask ); // nBitmap and nMask ownership transferred |
| |
| EikSoftkeyImage::SetImage(cba, *myimage, left); // Takes myimage ownership |
| cbaHasImage[position] = true; |
| ret = true; |
| } else { |
| // Restore softkey to text based |
| if (cbaHasImage[position]) { |
| EikSoftkeyImage::SetLabel(cba, left); |
| cbaHasImage[position] = false; |
| } |
| } |
| } |
| return ret; |
| } |
| |
| bool QSoftKeyManagerPrivateS60::setSoftkey(CEikButtonGroupContainer &cba, |
| QAction::SoftKeyRole role, int position) |
| { |
| QAction *action = highestPrioritySoftkey(role); |
| if (action) { |
| setSoftkeyImage(&cba, *action, position); |
| QString text = softkeyText(*action); |
| TPtrC nativeText = qt_QString2TPtrC(text); |
| int command = S60_COMMAND_START + position; |
| #if defined(Q_WS_S60) && !defined(SYMBIAN_VERSION_9_4) && !defined(SYMBIAN_VERSION_9_3) && !defined(SYMBIAN_VERSION_9_2) |
| if (softKeyCommandActions.contains(action)) |
| command = softKeyCommandActions.value(action); |
| #endif |
| setNativeSoftkey(cba, position, command, nativeText); |
| const bool dimmed = !action->isEnabled() && !QSoftKeyManager::isForceEnabledInSofkeys(action); |
| cba.DimCommand(command, dimmed); |
| realSoftKeyActions.insert(command, action); |
| return true; |
| } |
| return false; |
| } |
| |
| bool QSoftKeyManagerPrivateS60::setLeftSoftkey(CEikButtonGroupContainer &cba) |
| { |
| return setSoftkey(cba, QAction::PositiveSoftKey, LSK_POSITION); |
| } |
| |
| bool QSoftKeyManagerPrivateS60::setMiddleSoftkey(CEikButtonGroupContainer &cba) |
| { |
| // Note: In order to get MSK working, application has to have EAknEnableMSK flag set |
| // Currently it is not possible very easily) |
| // For more information see: http://wiki.forum.nokia.com/index.php/Middle_softkey_usage |
| return setSoftkey(cba, QAction::SelectSoftKey, MSK_POSITION); |
| } |
| |
| bool QSoftKeyManagerPrivateS60::setRightSoftkey(CEikButtonGroupContainer &cba) |
| { |
| if (!setSoftkey(cba, QAction::NegativeSoftKey, RSK_POSITION)) { |
| const Qt::WindowType windowType = initialSoftKeySource |
| ? initialSoftKeySource->window()->windowType() : Qt::Window; |
| if (windowType != Qt::Dialog && windowType != Qt::Popup) { |
| QString text(QSoftKeyManager::tr("Exit")); |
| TPtrC nativeText = qt_QString2TPtrC(text); |
| if (cbaHasImage[RSK_POSITION]) { |
| EikSoftkeyImage::SetLabel(&cba, false); |
| cbaHasImage[RSK_POSITION] = false; |
| } |
| setNativeSoftkey(cba, RSK_POSITION, EAknSoftkeyExit, nativeText); |
| cba.DimCommand(EAknSoftkeyExit, false); |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| void QSoftKeyManagerPrivateS60::setSoftkeys(CEikButtonGroupContainer &cba) |
| { |
| int requestedSoftkeyCount = requestedSoftKeyActions.count(); |
| const int maxSoftkeyCount = 2; // TODO: differs based on orientation ans S60 versions (some have MSK) |
| if (requestedSoftkeyCount > maxSoftkeyCount) { |
| // We have more softkeys than available slots |
| // Put highest priority negative action to RSK and Options menu with rest of softkey actions to LSK |
| // TODO: Build menu |
| setLeftSoftkey(cba); |
| if(AknLayoutUtils::MSKEnabled()) |
| setMiddleSoftkey(cba); |
| setRightSoftkey(cba); |
| } else { |
| // We have less softkeys than available slots |
| // Put softkeys to request slots based on role |
| setLeftSoftkey(cba); |
| if(AknLayoutUtils::MSKEnabled()) |
| setMiddleSoftkey(cba); |
| setRightSoftkey(cba); |
| } |
| } |
| |
| void QSoftKeyManagerPrivateS60::updateSoftKeys_sys() |
| { |
| if (skipCbaUpdate()) |
| return; |
| |
| CEikButtonGroupContainer *nativeContainer = S60->buttonGroupContainer(); |
| Q_ASSERT_X(nativeContainer, Q_FUNC_INFO, "Native CBA does not exist!"); |
| ensureCbaVisibilityAndResponsiviness(*nativeContainer); |
| clearSoftkeys(*nativeContainer); |
| setSoftkeys(*nativeContainer); |
| |
| nativeContainer->DrawDeferred(); // 3.1 needs an extra invitation |
| } |
| |
| static void resetMenuBeingConstructed(TAny* /*aAny*/) |
| { |
| S60->menuBeingConstructed = false; |
| } |
| |
| void QSoftKeyManagerPrivateS60::tryDisplayMenuBarL() |
| { |
| CleanupStack::PushL(TCleanupItem(resetMenuBeingConstructed, NULL)); |
| S60->menuBeingConstructed = true; |
| S60->menuBar()->TryDisplayMenuBarL(); |
| CleanupStack::PopAndDestroy(); // Reset menuBeingConstructed to false in all cases |
| } |
| |
| bool QSoftKeyManagerPrivateS60::handleCommand(int command) |
| { |
| QAction *action = realSoftKeyActions.value(command); |
| if (action) { |
| bool property = QActionPrivate::get(action)->menuActionSoftkeys; |
| if (property) { |
| QT_TRAP_THROWING(tryDisplayMenuBarL()); |
| } else if (action->menu()) { |
| // TODO: This is hack, in order to use exising QMenuBar implementation for Symbian |
| // menubar needs to have widget to which it is associated. Since we want to associate |
| // menubar to action (which is inherited from QObject), we create and associate QWidget |
| // to action and pass that for QMenuBar. This associates the menubar to action, and we |
| // can have own menubar for each action. |
| QWidget *actionContainer = action->property("_q_action_widget").value<QWidget*>(); |
| if(!actionContainer) { |
| actionContainer = new QWidget(action->parentWidget()); |
| QMenuBar *menuBar = new QMenuBar(actionContainer); |
| foreach(QAction *menuAction, action->menu()->actions()) { |
| QMenu *menu = menuAction->menu(); |
| if(menu) |
| menuBar->addMenu(menu); |
| else |
| menuBar->addAction(menuAction); |
| } |
| QVariant v; |
| v.setValue(actionContainer); |
| action->setProperty("_q_action_widget", v); |
| } |
| qt_symbian_next_menu_from_action(actionContainer); |
| QT_TRAP_THROWING(tryDisplayMenuBarL()); |
| } |
| |
| Q_ASSERT(action->softKeyRole() != QAction::NoSoftKey); |
| QWidget *actionParent = action->parentWidget(); |
| Q_ASSERT_X(actionParent, Q_FUNC_INFO, "No parent set for softkey action!"); |
| if (actionParent->isEnabled()) { |
| action->activate(QAction::Trigger); |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| QT_END_NAMESPACE |
| #endif //QT_NO_SOFTKEYMANAGER |