1 /****************************************************************************
2 **
3 ** Copyright (C) 2015 The Qt Company Ltd.
4 ** Contact: http://www.qt.io/licensing/
5 **
6 ** This file is part of the QtGui module of the Qt Toolkit.
7 **
8 ** $QT_BEGIN_LICENSE:LGPL$
9 ** Commercial License Usage
10 ** Licensees holding valid commercial Qt licenses may use this file in
11 ** accordance with the commercial license agreement provided with the
12 ** Software or, alternatively, in accordance with the terms contained in
13 ** a written agreement between you and The Qt Company. For licensing terms
14 ** and conditions see http://www.qt.io/terms-conditions. For further
15 ** information use the contact form at http://www.qt.io/contact-us.
16 **
17 ** GNU Lesser General Public License Usage
18 ** Alternatively, this file may be used under the terms of the GNU Lesser
19 ** General Public License version 2.1 or version 3 as published by the Free
20 ** Software Foundation and appearing in the file LICENSE.LGPLv21 and
21 ** LICENSE.LGPLv3 included in the packaging of this file. Please review the
22 ** following information to ensure the GNU Lesser General Public License
23 ** requirements will be met: https://www.gnu.org/licenses/lgpl.html and
24 ** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
25 **
26 ** As a special exception, The Qt Company gives you certain additional
27 ** rights. These rights are described in The Qt Company LGPL Exception
28 ** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
29 **
30 ** GNU General Public License Usage
31 ** Alternatively, this file may be used under the terms of the GNU
32 ** General Public License version 3.0 as published by the Free Software
33 ** Foundation and appearing in the file LICENSE.GPL included in the
34 ** packaging of this file.  Please review the following information to
35 ** ensure the GNU General Public License version 3.0 requirements will be
36 ** met: http://www.gnu.org/copyleft/gpl.html.
37 **
38 ** $QT_END_LICENSE$
39 **
40 ****************************************************************************/
41 
42 #include "qapplication.h"
43 #include "qevent.h"
44 #include "qbitmap.h"
45 #include "qstyle.h"
46 #include "qmenubar.h"
47 #include "private/qt_s60_p.h"
48 #include "private/qmenu_p.h"
49 #include "private/qaction_p.h"
50 #include "private/qsoftkeymanager_p.h"
51 #include "private/qsoftkeymanager_s60_p.h"
52 #include "private/qobject_p.h"
53 #include <eiksoftkeyimage.h>
54 #include <eikcmbut.h>
55 
56 #ifndef QT_NO_STYLE_S60
57 #include <qs60style.h>
58 #endif
59 
60 #ifndef QT_NO_SOFTKEYMANAGER
61 QT_BEGIN_NAMESPACE
62 
63 const int S60_COMMAND_START = 6000;
64 const int LSK_POSITION = 0;
65 const int MSK_POSITION = 3;
66 const int RSK_POSITION = 2;
67 
QSoftKeyManagerPrivateS60()68 QSoftKeyManagerPrivateS60::QSoftKeyManagerPrivateS60() : cbaHasImage(4) // 4 since MSK position index is 3
69 {
70     cachedCbaIconSize[0] = QSize(0,0);
71     cachedCbaIconSize[1] = QSize(0,0);
72     cachedCbaIconSize[2] = QSize(0,0);
73     cachedCbaIconSize[3] = QSize(0,0);
74 }
75 
skipCbaUpdate()76 bool QSoftKeyManagerPrivateS60::skipCbaUpdate()
77 {
78     // Lets not update softkeys if
79     // 1. We don't have application panes, i.e. cba
80     // 2. Our CBA is not active, i.e. S60 native dialog or menu with custom CBA is shown
81     //    2.1. Except if thre is no current CBA at all and WindowSoftkeysRespondHint is set
82 
83     // Note: Cannot use IsDisplayingMenuOrDialog since CBA update can be triggered before
84     // menu/dialog CBA is actually displayed i.e. it is being costructed.
85     CEikButtonGroupContainer *appUiCba = S60->buttonGroupContainer();
86     if (!appUiCba)
87         return true;
88     // CEikButtonGroupContainer::Current returns 0 if CBA is not visible at all
89     CEikButtonGroupContainer *currentCba = CEikButtonGroupContainer::Current();
90     // Check if softkey need to be update even they are not visible
91     bool cbaRespondsWhenInvisible = false;
92     QWidget *window = QApplication::activeWindow();
93     if (window && (window->windowFlags() & Qt::WindowSoftkeysRespondHint))
94         cbaRespondsWhenInvisible = true;
95 
96     if (QApplication::testAttribute(Qt::AA_S60DontConstructApplicationPanes)
97             || (appUiCba != currentCba && !cbaRespondsWhenInvisible)) {
98         return true;
99     }
100     return false;
101 }
102 
ensureCbaVisibilityAndResponsiviness(CEikButtonGroupContainer & cba)103 void QSoftKeyManagerPrivateS60::ensureCbaVisibilityAndResponsiviness(CEikButtonGroupContainer &cba)
104 {
105     RDrawableWindow *cbaWindow = cba.DrawableWindow();
106     Q_ASSERT_X(cbaWindow, Q_FUNC_INFO, "Native CBA does not have window!");
107     // CBA comes on top of new option menu
108     int pos = 0;
109 
110     if(cba.ButtonGroupType()== SLafButtonGroupContainer::ECba)
111         pos = 1;
112 
113     cbaWindow->SetOrdinalPosition(pos);
114     // Qt shares same CBA instance between top-level widgets,
115     // make sure we are not faded by underlying window.
116     cbaWindow->SetFaded(EFalse, RWindowTreeNode::EFadeIncludeChildren);
117     // Modal dialogs capture pointer events, but shared cba instance
118     // shall stay responsive. Raise pointer capture priority to keep
119     // softkeys responsive in modal dialogs
120     cbaWindow->SetPointerCapturePriority(1);
121 }
122 
clearSoftkeys(CEikButtonGroupContainer & cba)123 void QSoftKeyManagerPrivateS60::clearSoftkeys(CEikButtonGroupContainer &cba)
124 {
125 #if defined(Q_WS_S60) && !defined(SYMBIAN_VERSION_9_4) && !defined(SYMBIAN_VERSION_9_3) && !defined(SYMBIAN_VERSION_9_2)
126     QT_TRAP_THROWING(
127         //EAknSoftkeyEmpty is used, because using -1 adds softkeys without actions on Symbian3
128         cba.SetCommandL(0, EAknSoftkeyEmpty, KNullDesC);
129         cba.SetCommandL(2, EAknSoftkeyEmpty, KNullDesC);
130     );
131 #else
132     QT_TRAP_THROWING(
133         //Using -1 instead of EAknSoftkeyEmpty to avoid flickering.
134         cba.SetCommandL(0, -1, KNullDesC);
135         // TODO: Should we clear also middle SK?
136         cba.SetCommandL(2, -1, KNullDesC);
137     );
138 #endif
139     realSoftKeyActions.clear();
140 }
141 
softkeyText(QAction & softkeyAction)142 QString QSoftKeyManagerPrivateS60::softkeyText(QAction &softkeyAction)
143 {
144     // In S60 softkeys and menu items do not support key accelerators (i.e.
145     // CTRL+X). Therefore, removing the accelerator characters from both softkey
146     // and menu item texts.
147     const int underlineShortCut = QApplication::style()->styleHint(QStyle::SH_UnderlineShortcut);
148     QString iconText = softkeyAction.iconText();
149     return underlineShortCut ? softkeyAction.text() : iconText;
150 }
151 
highestPrioritySoftkey(QAction::SoftKeyRole role)152 QAction *QSoftKeyManagerPrivateS60::highestPrioritySoftkey(QAction::SoftKeyRole role)
153 {
154     QAction *ret = NULL;
155     // Priority look up is two level
156     // 1. First widget with softkeys always has highest priority
157     for (int level = 0; !ret; level++) {
158         // 2. Highest priority action within widget
159         QList<QAction*> actions = requestedSoftKeyActions.values(level);
160         if (actions.isEmpty())
161             break;
162         qSort(actions.begin(), actions.end(), QSoftKeyManagerPrivateS60::actionPriorityMoreThan);
163         foreach (QAction *action, actions) {
164             if (action->softKeyRole() == role) {
165                 ret = action;
166                 break;
167             }
168         }
169     }
170     return ret;
171 }
172 
actionPriorityMoreThan(const QAction * firstItem,const QAction * secondItem)173 bool QSoftKeyManagerPrivateS60::actionPriorityMoreThan(const QAction *firstItem, const QAction *secondItem)
174 {
175     return firstItem->priority() > secondItem->priority();
176 }
177 
setNativeSoftkey(CEikButtonGroupContainer & cba,TInt position,TInt command,const TDesC & text)178 void QSoftKeyManagerPrivateS60::setNativeSoftkey(CEikButtonGroupContainer &cba,
179                                               TInt position, TInt command, const TDesC &text)
180 {
181     // Calling SetCommandL causes CBA redraw
182     QT_TRAP_THROWING(cba.SetCommandL(position, command, text));
183 }
184 
softkeyIconPosition(int position,QSize sourceSize,QSize targetSize)185 QPoint QSoftKeyManagerPrivateS60::softkeyIconPosition(int position, QSize sourceSize, QSize targetSize)
186 {
187     QPoint iconPosition(0,0);
188 
189     // Prior to S60 5.3 icons need to be properly positioned to buttons, but starting with 5.3
190     // positioning is done on Avkon side.
191     if (QSysInfo::s60Version() < QSysInfo::SV_S60_5_3) {
192         switch (AknLayoutUtils::CbaLocation())
193             {
194             case AknLayoutUtils::EAknCbaLocationBottom:
195                 // RSK must be moved to right, LSK in on correct position by default
196                 if (position == RSK_POSITION)
197                     iconPosition.setX(targetSize.width() - sourceSize.width());
198                 break;
199             case AknLayoutUtils::EAknCbaLocationRight:
200             case AknLayoutUtils::EAknCbaLocationLeft:
201                 // Already in correct position
202             default:
203                 break;
204             }
205 
206         // Align horizontally to center
207         iconPosition.setY((targetSize.height() - sourceSize.height()) >> 1);
208     }
209     return iconPosition;
210 }
211 
prepareSoftkeyPixmap(QPixmap src,int position,QSize targetSize)212 QPixmap QSoftKeyManagerPrivateS60::prepareSoftkeyPixmap(QPixmap src, int position, QSize targetSize)
213 {
214     QPixmap target(targetSize);
215     target.fill(Qt::transparent);
216     QPainter p;
217     p.begin(&target);
218     p.drawPixmap(softkeyIconPosition(position, src.size(), targetSize), src);
219     p.end();
220     return target;
221 }
222 
isOrientationLandscape()223 bool QSoftKeyManagerPrivateS60::isOrientationLandscape()
224 {
225     // Hard to believe that there is no public API in S60 to
226     // get current orientation. This workaround works with currently supported resolutions
227     return  S60->screenHeightInPixels <  S60->screenWidthInPixels;
228 }
229 
cbaIconSize(CEikButtonGroupContainer * cba,int position)230 QSize QSoftKeyManagerPrivateS60::cbaIconSize(CEikButtonGroupContainer *cba, int position)
231 {
232     int index = position;
233     index += isOrientationLandscape() ? 0 : 1;
234     if(cachedCbaIconSize[index].isNull()) {
235         if (QSysInfo::s60Version() >= QSysInfo::SV_S60_5_3) {
236             // S60 5.3 and later have fixed icon size on softkeys, while the button
237             // itself is bigger, so the automatic check doesn't work.
238             // Use custom pixel metrics to deduce the CBA icon size
239             int iconHeight = 30;
240             int iconWidth = 30;
241 #ifndef QT_NO_STYLE_S60
242             QS60Style *s60Style = 0;
243             s60Style = qobject_cast<QS60Style *>(QApplication::style());
244             if (s60Style) {
245                 iconWidth = s60Style->pixelMetric((QStyle::PixelMetric)PM_CbaIconWidth);
246                 iconHeight = s60Style->pixelMetric((QStyle::PixelMetric)PM_CbaIconHeight);
247             }
248 #endif
249             cachedCbaIconSize[index] = QSize(iconWidth, iconHeight);
250         } else {
251             // Only way I figured out to get CBA icon size without RnD SDK, was
252             // to set some dummy icon to CBA first and then ask CBA button CCoeControl::Size()
253             // The returned value is cached to avoid unnecessary icon setting every time.
254             const bool left = (position == LSK_POSITION);
255             if (position == LSK_POSITION || position == RSK_POSITION) {
256                 CEikImage* tmpImage = NULL;
257                 QT_TRAP_THROWING(tmpImage = new (ELeave) CEikImage);
258                 EikSoftkeyImage::SetImage(cba, *tmpImage, left); // Takes tmpImage ownership
259                 int command = S60_COMMAND_START + position;
260                 setNativeSoftkey(*cba, position, command, KNullDesC());
261                 cachedCbaIconSize[index] = qt_TSize2QSize(cba->ControlOrNull(command)->Size());
262                 EikSoftkeyImage::SetLabel(cba, left);
263 
264                 if (cachedCbaIconSize[index] == QSize(138,72)) {
265                     // Hack for S60 5.0 landscape orientation, which return wrong icon size
266                     cachedCbaIconSize[index] = QSize(60,60);
267                 }
268             }
269         }
270     }
271 
272     return cachedCbaIconSize[index];
273 }
274 
setSoftkeyImage(CEikButtonGroupContainer * cba,QAction & action,int position)275 bool QSoftKeyManagerPrivateS60::setSoftkeyImage(CEikButtonGroupContainer *cba,
276                                             QAction &action, int position)
277 {
278     bool ret = false;
279 
280     const bool left = (position == LSK_POSITION);
281     if(position == LSK_POSITION || position == RSK_POSITION) {
282         QIcon icon = action.icon();
283         if (!icon.isNull()) {
284             // Get size of CBA icon area based on button position and orientation
285             QSize requiredIconSize = cbaIconSize(cba, position);
286             // Get pixmap out of icon based on preferred size, the aspect ratio is kept
287             QPixmap pmWihtAspectRatio = icon.pixmap(requiredIconSize);
288             // Native softkeys require that pixmap size is exactly the same as requiredIconSize
289             // prepareSoftkeyPixmap creates a new pixmap with requiredIconSize and blits the 'pmWihtAspectRatio'
290             // to correct location of it
291             QPixmap softkeyPixmap = prepareSoftkeyPixmap(pmWihtAspectRatio, position, requiredIconSize);
292 
293             QPixmap softkeyAlpha = softkeyPixmap.alphaChannel();
294             // Alpha channel in 5.1 and older devices need to be inverted
295             // TODO: Switch to use toSymbianCFbsBitmap with invert when available
296             if(QSysInfo::s60Version() <= QSysInfo::SV_S60_5_1) {
297                 QImage alphaImage = softkeyAlpha.toImage();
298                 alphaImage.invertPixels();
299                 softkeyAlpha = QPixmap::fromImage(alphaImage);
300             }
301 
302             CFbsBitmap* nBitmap = softkeyPixmap.toSymbianCFbsBitmap();
303             CFbsBitmap* nMask = softkeyAlpha.toSymbianCFbsBitmap();
304 
305             CEikImage* myimage = new (ELeave) CEikImage;
306             myimage->SetPicture( nBitmap, nMask ); // nBitmap and nMask ownership transferred
307 
308             EikSoftkeyImage::SetImage(cba, *myimage, left); // Takes myimage ownership
309             cbaHasImage[position] = true;
310             ret = true;
311         }
312     }
313     return ret;
314 }
315 
setSoftkey(CEikButtonGroupContainer & cba,QAction::SoftKeyRole role,int position)316 bool QSoftKeyManagerPrivateS60::setSoftkey(CEikButtonGroupContainer &cba,
317                                         QAction::SoftKeyRole role, int position)
318 {
319     QAction *action = highestPrioritySoftkey(role);
320     if (action) {
321         bool hasImage = setSoftkeyImage(&cba, *action, position);
322         QString text = softkeyText(*action);
323         TPtrC nativeText = qt_QString2TPtrC(text);
324         int command = S60_COMMAND_START + position;
325 #if defined(Q_WS_S60) && !defined(SYMBIAN_VERSION_9_4) && !defined(SYMBIAN_VERSION_9_3) && !defined(SYMBIAN_VERSION_9_2)
326         if (softKeyCommandActions.contains(action))
327             command = softKeyCommandActions.value(action);
328 #endif
329         setNativeSoftkey(cba, position, command, nativeText);
330         if (!hasImage && cbaHasImage[position]) {
331             EikSoftkeyImage::SetLabel(&cba, (position == LSK_POSITION));
332             cbaHasImage[position] = false;
333         }
334 
335         const bool dimmed = !action->isEnabled() && !QSoftKeyManager::isForceEnabledInSofkeys(action);
336         cba.DimCommand(command, dimmed);
337         realSoftKeyActions.insert(command, action);
338         return true;
339     }
340     return false;
341 }
342 
setLeftSoftkey(CEikButtonGroupContainer & cba)343 bool QSoftKeyManagerPrivateS60::setLeftSoftkey(CEikButtonGroupContainer &cba)
344 {
345     if (!setSoftkey(cba, QAction::PositiveSoftKey, LSK_POSITION)) {
346         if (cbaHasImage[LSK_POSITION]) {
347             // Clear any residual icon if LSK has no action. A real softkey
348             // is needed for SetLabel command to work, so do a temporary dummy
349             setNativeSoftkey(cba, LSK_POSITION, EAknSoftkeyExit, KNullDesC);
350             EikSoftkeyImage::SetLabel(&cba, true);
351             setNativeSoftkey(cba, LSK_POSITION, EAknSoftkeyEmpty, KNullDesC);
352             cbaHasImage[LSK_POSITION] = false;
353         }
354         return false;
355     }
356     return true;
357 }
358 
setMiddleSoftkey(CEikButtonGroupContainer & cba)359 bool QSoftKeyManagerPrivateS60::setMiddleSoftkey(CEikButtonGroupContainer &cba)
360 {
361     // Note: In order to get MSK working, application has to have EAknEnableMSK flag set
362     // Currently it is not possible very easily)
363     // For more information see: http://wiki.forum.nokia.com/index.php/Middle_softkey_usage
364     return setSoftkey(cba, QAction::SelectSoftKey, MSK_POSITION);
365 }
366 
setRightSoftkey(CEikButtonGroupContainer & cba)367 bool QSoftKeyManagerPrivateS60::setRightSoftkey(CEikButtonGroupContainer &cba)
368 {
369     if (!setSoftkey(cba, QAction::NegativeSoftKey, RSK_POSITION)) {
370         const Qt::WindowType windowType = initialSoftKeySource
371             ? initialSoftKeySource->window()->windowType() : Qt::Window;
372         if (windowType != Qt::Dialog && windowType != Qt::Popup) {
373             QString text(QSoftKeyManager::tr("Exit"));
374             TPtrC nativeText = qt_QString2TPtrC(text);
375             setNativeSoftkey(cba, RSK_POSITION, EAknSoftkeyExit, nativeText);
376             if (cbaHasImage[RSK_POSITION]) {
377                 EikSoftkeyImage::SetLabel(&cba, false);
378                 cbaHasImage[RSK_POSITION] = false;
379             }
380             cba.DimCommand(EAknSoftkeyExit, false);
381             return true;
382         } else {
383             if (cbaHasImage[RSK_POSITION]) {
384                 // Clear any residual icon if RSK has no action. A real softkey
385                 // is needed for SetLabel command to work, so do a temporary dummy
386                 setNativeSoftkey(cba, RSK_POSITION, EAknSoftkeyExit, KNullDesC);
387                 EikSoftkeyImage::SetLabel(&cba, false);
388                 setNativeSoftkey(cba, RSK_POSITION, EAknSoftkeyEmpty, KNullDesC);
389                 cbaHasImage[RSK_POSITION] = false;
390             }
391             return false;
392         }
393     }
394     return true;
395 }
396 
setSoftkeys(CEikButtonGroupContainer & cba)397 void QSoftKeyManagerPrivateS60::setSoftkeys(CEikButtonGroupContainer &cba)
398 {
399     int requestedSoftkeyCount = requestedSoftKeyActions.count();
400     const int maxSoftkeyCount = 2; // TODO: differs based on orientation ans S60 versions (some have MSK)
401     if (requestedSoftkeyCount > maxSoftkeyCount) {
402         // We have more softkeys than available slots
403         // Put highest priority negative action to RSK and Options menu with rest of softkey actions to LSK
404         // TODO: Build menu
405         setLeftSoftkey(cba);
406         if(AknLayoutUtils::MSKEnabled())
407             setMiddleSoftkey(cba);
408         setRightSoftkey(cba);
409     } else {
410         // We have less softkeys than available slots
411         // Put softkeys to request slots based on role
412         setLeftSoftkey(cba);
413         if(AknLayoutUtils::MSKEnabled())
414             setMiddleSoftkey(cba);
415         setRightSoftkey(cba);
416     }
417 }
418 
updateSoftKeys_sys()419 void QSoftKeyManagerPrivateS60::updateSoftKeys_sys()
420 {
421     if (skipCbaUpdate())
422         return;
423 
424     CEikButtonGroupContainer *nativeContainer = S60->buttonGroupContainer();
425     Q_ASSERT_X(nativeContainer, Q_FUNC_INFO, "Native CBA does not exist!");
426     ensureCbaVisibilityAndResponsiviness(*nativeContainer);
427     clearSoftkeys(*nativeContainer);
428     setSoftkeys(*nativeContainer);
429 
430     nativeContainer->DrawDeferred(); // 3.1 needs an extra invitation
431 }
432 
resetMenuBeingConstructed(TAny *)433 static void resetMenuBeingConstructed(TAny* /*aAny*/)
434 {
435     S60->menuBeingConstructed = false;
436 }
437 
tryDisplayMenuBarL()438 void QSoftKeyManagerPrivateS60::tryDisplayMenuBarL()
439 {
440     CleanupStack::PushL(TCleanupItem(resetMenuBeingConstructed, NULL));
441     S60->menuBeingConstructed = true;
442     S60->menuBar()->TryDisplayMenuBarL();
443     CleanupStack::PopAndDestroy(); // Reset menuBeingConstructed to false in all cases
444 }
445 
handleCommand(int command)446 bool QSoftKeyManagerPrivateS60::handleCommand(int command)
447 {
448     QAction *action = realSoftKeyActions.value(command);
449     if (action) {
450         bool property = QActionPrivate::get(action)->menuActionSoftkeys;
451         if (property) {
452             QT_TRAP_THROWING(tryDisplayMenuBarL());
453         } else if (action->menu()) {
454             // TODO: This is hack, in order to use exising QMenuBar implementation for Symbian
455             // menubar needs to have widget to which it is associated. Since we want to associate
456             // menubar to action (which is inherited from QObject), we create and associate QWidget
457             // to action and pass that for QMenuBar. This associates the menubar to action, and we
458             // can have own menubar for each action.
459             QWidget *actionContainer = action->property("_q_action_widget").value<QWidget*>();
460             if(!actionContainer) {
461                 actionContainer = new QWidget(action->parentWidget());
462                 QMenuBar *menuBar = new QMenuBar(actionContainer);
463                 foreach(QAction *menuAction, action->menu()->actions()) {
464                     QMenu *menu = menuAction->menu();
465                     if(menu)
466                         menuBar->addMenu(menu);
467                     else
468                         menuBar->addAction(menuAction);
469                 }
470                 QVariant v;
471                 v.setValue(actionContainer);
472                 action->setProperty("_q_action_widget", v);
473             }
474             qt_symbian_next_menu_from_action(actionContainer);
475             QT_TRAP_THROWING(tryDisplayMenuBarL());
476         }
477 
478         Q_ASSERT(action->softKeyRole() != QAction::NoSoftKey);
479         QWidget *actionParent = action->parentWidget();
480         Q_ASSERT_X(actionParent, Q_FUNC_INFO, "No parent set for softkey action!");
481         if (actionParent->isEnabled()) {
482             action->activate(QAction::Trigger);
483             return true;
484         }
485     }
486     return false;
487 }
488 
489 QT_END_NAMESPACE
490 #endif //QT_NO_SOFTKEYMANAGER
491