Merge lp:~zsombi/ubuntu-ui-toolkit/contextual_actions into lp:ubuntu-ui-toolkit/staging

Proposed by Zsombor Egri
Status: Merged
Approved by: Zsombor Egri
Approved revision: 1800
Merged at revision: 1804
Proposed branch: lp:~zsombi/ubuntu-ui-toolkit/contextual_actions
Merge into: lp:ubuntu-ui-toolkit/staging
Diff against target: 1404 lines (+785/-114)
19 files modified
components.api (+3/-0)
examples/ubuntu-ui-toolkit-gallery/gallery-logging.config (+3/-0)
src/Ubuntu/Components/1.3/MainViewBase.qml (+13/-0)
src/Ubuntu/Components/1.3/Page.qml (+19/-4)
src/Ubuntu/Components/Popups/1.3/Dialog.qml (+7/-0)
src/Ubuntu/Components/Popups/1.3/Popover.qml (+8/-0)
src/Ubuntu/Components/plugin/adapters/actionsproxy_p.cpp (+73/-63)
src/Ubuntu/Components/plugin/adapters/actionsproxy_p.h (+8/-7)
src/Ubuntu/Components/plugin/plugin.cpp (+1/-0)
src/Ubuntu/Components/plugin/ucaction.cpp (+65/-9)
src/Ubuntu/Components/plugin/ucaction.h (+18/-0)
src/Ubuntu/Components/plugin/ucactioncontext.cpp (+138/-9)
src/Ubuntu/Components/plugin/ucactioncontext.h (+45/-3)
src/Ubuntu/Components/plugin/ucactionitem.cpp (+3/-1)
src/Ubuntu/Components/plugin/ucbottomedge_p.h (+0/-1)
src/Ubuntu/Components/plugin/uclistitem.cpp (+9/-3)
tests/unit/tst_components/tst_action.qml (+14/-6)
tests/unit_x11/tst_components/tst_contextual_actions.qml (+326/-0)
tests/unit_x11/tst_components/tst_shortcuts.qml (+32/-8)
To merge this branch: bzr merge lp:~zsombi/ubuntu-ui-toolkit/contextual_actions
Reviewer Review Type Date Requested Status
PS Jenkins bot continuous-integration Approve
Cris Dywan Approve
ubuntu-sdk-build-bot continuous-integration Needs Fixing
Review via email: mp+281143@code.launchpad.net

Commit message

Contextual Actions - shortcut (and mnemonics) handling must obey active/inactive contexts, such as Page activation as well as Popups and Dialogs.

Description of the change

Contextual Actions - shortcut (and mnemonics) handling must obey active/inactive contexts, such as Page activation as well as Popups and Dialogs.

To post a comment you must log in.
1789. By Zsombor Egri

fix ActionContext behavior

1790. By Zsombor Egri

API fixed

1791. By Zsombor Egri

rolling back unwanted changes

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
1792. By Zsombor Egri

register active contexts

1793. By Zsombor Egri

PopupContext in MainView, Popover and Dialog

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
1794. By Zsombor Egri

lightwaight ActionProxy; tests added

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
Cris Dywan (kalikiana) wrote :

+ bool activable = window && window == QGuiApplication::focusWindow();

activable -> activatable

 * ActionContext drives the Action objects declared within the ActionContext
 * through the \l actions list. Beside that the ActionContext drives the activability
 * of Action objects' shortcuts declared in a hierarchy. In the following
 * example the ActionContext drives the underlaying \c action1 and \c action2
 * shortcuts:

Brought to you by redundancy department of redundancy ;-)

How about something simpler:

ActionContext drives the state of its \l actions. Shortcuts and mnemonics are only registered if the context is active.

But I find the example a bit confusing. There's a rootContext with an action responding to ^A but at the end of the example you're explaining how this cannot work... and the buttons are not using the context at all - how is this an example of using the context?

+ Q_REVISION(1) void popupChanged();

There is a signal yet there's no Q_PROPERTY?

I'd say either it's fully internal or it becomes a property proper.

+ * \note An Action declared to a component alling under an item that is a child of

alling?

+ * The toolkit provides such kind of contexts in MainView, Popup and Dialog. It is

such kind -> this kind

+ bool m_effectiveActive:1;

effectivelyActive

review: Needs Fixing
1795. By Zsombor Egri

shortcuts can only trigger an Action if that action is assigned to an active context or to an ActionItem

1796. By Zsombor Egri

too much delay between tests in contextual actions

1797. By Zsombor Egri

staging sync

Revision history for this message
Zsombor Egri (zsombi) wrote :

> + bool activable = window && window == QGuiApplication::focusWindow();
>
> activable -> activatable

Ehm... ok :) it sounded good to me :)

>
> * ActionContext drives the Action objects declared within the ActionContext
> * through the \l actions list. Beside that the ActionContext drives the
> activability
> * of Action objects' shortcuts declared in a hierarchy. In the following
> * example the ActionContext drives the underlaying \c action1 and \c action2
> * shortcuts:
>
> Brought to you by redundancy department of redundancy ;-)

LOL, completely agree... it's a bit of a cumbersome description. However there's something right in it :)

>
> How about something simpler:
>
> ActionContext drives the state of its \l actions. Shortcuts and mnemonics are
> only registered if the context is active.

The second sentence is not true. The shortcuts are registered but the action to which they are registered will be activated only if the action is in an active ActionContext or assigned to an ActionItem which is declared as a child to an item which has an active ActionContext, and all its ancestors who have ActionContext declared are active... huh... we need a better description of this...

>
> But I find the example a bit confusing. There's a rootContext with an action
> responding to ^A but at the end of the example you're explaining how this
> cannot work... and the buttons are not using the context at all - how is this
> an example of using the context?

So, in the example the Rectangle has an active ActionContext. The Buttons within have Actions assigned to the ActionItem, so the first requirement (the Action is assigned to an ActionItem) is fulfilled. When the shortcut triggers in either of the Button actions, the selection logic checks whether the ActionItem's ancestors have all ActionContexts active. If yes, it marks selected, so it can trigger. Then the first button can deactivate the context, in which case neither of the actions will be selected for the registered shortcut.

To ease the detection, ActionContext - upon component creation - attaches an object to its parent, so we can detect which parent Item has an ActionContext object declared.

Summarising: An Action can be triggered by a shortcut (or mnemonic) if a) it is declared in an active context action (ActionContext.actions) or b) it is assigned to an ActionItem (or derivate) which has (at least one) ancestor Items which have active ActionContexts.

>
> + Q_REVISION(1) void popupChanged();
>
> There is a signal yet there's no Q_PROPERTY?
>
> I'd say either it's fully internal or it becomes a property proper.
>

That's a leftover, needs to be cut.

> + * \note An Action declared to a component alling under an item that is a
> child of
>
> calling?

falling

>
> + * The toolkit provides such kind of contexts in MainView, Popup and Dialog.
> It is
>
> such kind -> this kind

Fixed

>
> + bool m_effectiveActive:1;
>
> effectivelyActive

I followed QtQuick naming, they use effectiveVisible, effectiveLayoutMirror, effectiveEnabled, etc

1798. By Zsombor Egri

few review comments applied

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Approve (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Approve (continuous-integration)
Revision history for this message
ubuntu-sdk-build-bot (ubuntu-sdk-build-bot) :
review: Approve (continuous-integration)
Revision history for this message
ubuntu-sdk-build-bot (ubuntu-sdk-build-bot) wrote :

FAILED: Autolanding.
Approved revid is not set in launchpad. This is most likely a launchpad issue and re-approve should fix it. There is also a chance (although a very small one) this is a permission problem of the ps-jenkins bot.
https://jenkins.ubuntu.com/ubuntu-sdk/job/ubuntu-ui-toolkit-autolanding/5/
Executed test runs:
    None: https://jenkins.ubuntu.com/ubuntu-sdk/job/generic-land-mp/5/console

review: Needs Fixing (continuous-integration)
Revision history for this message
ubuntu-sdk-build-bot (ubuntu-sdk-build-bot) :
review: Approve (continuous-integration)
Revision history for this message
ubuntu-sdk-build-bot (ubuntu-sdk-build-bot) wrote :

FAILED: Autolanding.
Approved revid is not set in launchpad. This is most likely a launchpad issue and re-approve should fix it. There is also a chance (although a very small one) this is a permission problem of the ps-jenkins bot.
https://jenkins.ubuntu.com/ubuntu-sdk/job/ubuntu-ui-toolkit-autolanding/20/
Executed test runs:
    None: https://jenkins.ubuntu.com/ubuntu-sdk/job/generic-land-mp/20/console

review: Needs Fixing (continuous-integration)
Revision history for this message
Cris Dywan (kalikiana) wrote :
Download full text (3.2 KiB)

> > * ActionContext drives the Action objects declared within the ActionContext
> > * through the \l actions list. Beside that the ActionContext drives the
> > activability
> > * of Action objects' shortcuts declared in a hierarchy. In the following
> > * example the ActionContext drives the underlaying \c action1 and \c
> action2
> > * shortcuts:
> >
> > Brought to you by redundancy department of redundancy ;-)
>
> LOL, completely agree... it's a bit of a cumbersome description. However
> there's something right in it :)
>
> >
> > How about something simpler:
> >
> > ActionContext drives the state of its \l actions. Shortcuts and mnemonics
> > are only registered if the context is active.
>
> The second sentence is not true. The shortcuts are registered but the action
> to which they are registered will be activated only if the action is in an
> active ActionContext or assigned to an ActionItem which is declared as a child
> to an item which has an active ActionContext, and all its ancestors who have
> ActionContext declared are active... huh... we need a better description of
> this...

By registered I meant "will invoke its action when pressed".

How about:

ActionContext drives the state of its \l actions. Shortcuts and mnemonics are only registered if the context is active or if the action is assigned to an \l ActionItem all of whose parent contexts are active.

> > But I find the example a bit confusing. There's a rootContext with an action
> > responding to ^A but at the end of the example you're explaining how this
> > cannot work... and the buttons are not using the context at all - how is
> > this an example of using the context?
>
> So, in the example the Rectangle has an active ActionContext. The Buttons
> within have Actions assigned to the ActionItem, so the first requirement (the
> Action is assigned to an ActionItem) is fulfilled. When the shortcut triggers
> in either of the Button actions, the selection logic checks whether the
> ActionItem's ancestors have all ActionContexts active. If yes, it marks
> selected, so it can trigger. Then the first button can deactivate the context,
> in which case neither of the actions will be selected for the registered
> shortcut.
>
> To ease the detection, ActionContext - upon component creation - attaches an
> object to its parent, so we can detect which parent Item has an ActionContext
> object declared.
>
> Summarising: An Action can be triggered by a shortcut (or mnemonic) if a) it
> is declared in an active context action (ActionContext.actions) or b) it is
> assigned to an ActionItem (or derivate) which has (at least one) ancestor
> Items which have active ActionContexts.

So you are disagreeing with the explanation given in the example? Which says "rootContext will never be triggered through the \c {Ctrl+A} shortcut". But you are saying the rootContext can be used because the action is declared as a child. Which logically has to mean that its shortcut can work as well - the shortcut should be active whenever its Action is effectively active.

> > + bool m_effectiveActive:1;
> >
> > effectivelyActive
>
> I followed QtQuick naming, they use effectiveVisible, effectiveLa...

Read more...

review: Needs Fixing
Revision history for this message
Zsombor Egri (zsombi) wrote :

> By registered I meant "will invoke its action when pressed".
>
> How about:
>
> ActionContext drives the state of its \l actions. Shortcuts and mnemonics are
> only registered if the context is active or if the action is assigned to an \l
> ActionItem all of whose parent contexts are active.
>

Much better :)

> So you are disagreeing with the explanation given in the example? Which says
> "rootContext will never be triggered through the \c {Ctrl+A} shortcut". But
> you are saying the rootContext can be used because the action is declared as a
> child. Which logically has to mean that its shortcut can work as well - the
> shortcut should be active whenever its Action is effectively active.

Ou, hell, right! So I have to fix the sample and put an Action which is neither in an ActionContext nor assigned to an ActionItem!!!

1799. By Zsombor Egri

review comments applied, tests covering context owned action handling added

1800. By Zsombor Egri

staging sync

Revision history for this message
Cris Dywan (kalikiana) wrote :

Thanks for the updates and the added test. Looking nice now.

review: Approve
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Approve (continuous-integration)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'components.api'
2--- components.api 2015-12-19 10:06:24 +0000
3+++ components.api 2016-01-12 14:26:24 +0000
4@@ -715,6 +715,7 @@
5 Ubuntu.Components.Page 1.1 Page11: Page10
6 readonly property PageHeadConfiguration head
7 Ubuntu.Components.Page 1.3: PageTreeNode
8+ readonly property ActionContext actionContext
9 property Flickable flickable
10 readonly property PageHeadConfiguration head
11 property Item header
12@@ -947,6 +948,7 @@
13 property bool grabDismissAreaEvents
14 function var show()
15 function var hide()
16+Ubuntu.Components.PopupContext 1.3 UCPopupContext: ActionContext
17 Ubuntu.Components.Popups.PopupUtils 0.1 1.0 1.3
18 Ubuntu.Components.ProgressBar 1.0 0.1: AnimatedItem
19 property bool indeterminate
20@@ -1491,6 +1493,7 @@
21 property Item pageStack
22 Ubuntu.Components.Styles.ToolbarStyle 1.3: Item
23 property Component defaultDelegate
24+UCActionContextAttached: QtObject
25 Ubuntu.Components.UCApplication 1.0 0.1: QtObject
26 property string applicationName
27 property QtObject inputMethod
28
29=== modified file 'examples/ubuntu-ui-toolkit-gallery/gallery-logging.config'
30--- examples/ubuntu-ui-toolkit-gallery/gallery-logging.config 2015-12-11 10:24:51 +0000
31+++ examples/ubuntu-ui-toolkit-gallery/gallery-logging.config 2016-01-12 14:26:24 +0000
32@@ -4,3 +4,6 @@
33 ubuntu.components.SwipeArea.ActiveTouchInfo.debug=false
34 ubuntu.components.BottomEdge.debug=false
35 ubuntu.components.PageTreeNode.debug=false
36+ubuntu.components.Action.debug=false
37+ubuntu.components.ActionContext.debug=false
38+ubuntu.components.ActionProxy.debug=false
39
40=== modified file 'src/Ubuntu/Components/1.3/MainViewBase.qml'
41--- src/Ubuntu/Components/1.3/MainViewBase.qml 2015-12-08 18:34:40 +0000
42+++ src/Ubuntu/Components/1.3/MainViewBase.qml 2016-01-12 14:26:24 +0000
43@@ -159,6 +159,19 @@
44 }
45 }
46
47+ /*!
48+ \qmlproperty ActrionContext MainView::actionContext
49+ \readonly
50+ \since Ubuntu.Components 1.3
51+ The action context of the MainView.
52+ */
53+ readonly property alias actionContext: localContext
54+ Toolkit.PopupContext {
55+ id: localContext
56+ objectName: "RootContext"
57+ active: true
58+ }
59+
60 onApplicationNameChanged: {
61 if (applicationName !== "") {
62 i18n.domain = applicationName;
63
64=== modified file 'src/Ubuntu/Components/1.3/Page.qml'
65--- src/Ubuntu/Components/1.3/Page.qml 2015-12-08 18:34:40 +0000
66+++ src/Ubuntu/Components/1.3/Page.qml 2016-01-12 14:26:24 +0000
67@@ -15,12 +15,13 @@
68 */
69
70 import QtQuick 2.4
71-import Ubuntu.Components 1.3 as Toolkit13
72+import Ubuntu.Components 1.3
73 import "pageUtils.js" as Utils
74
75 /*!
76 \qmltype Page
77 \inqmlmodule Ubuntu.Components 1.1
78+ \inherits StyledItem
79 \ingroup ubuntu
80 \brief A page is the basic Item that must be used inside the \l MainView,
81 \l PageStack and \l Tabs.
82@@ -69,7 +70,7 @@
83 use a Page inside a Loader, but in that case do not set the anchors or size of the Loader
84 so that the Page can control its width and height.
85 */
86-Toolkit13.PageTreeNode {
87+PageTreeNode {
88 id: page
89 anchors {
90 left: parent ? parent.left : undefined
91@@ -81,6 +82,20 @@
92 height: parentNode ? page.flickable ? parentNode.height : parentNode.height - internal.headerHeight : undefined
93
94 /*!
95+ \qmlproperty ActrionContext Page::actionContext
96+ \readonly
97+ \since Ubuntu.Components 1.3
98+ The action context of the page.
99+ */
100+ readonly property alias actionContext: localContext
101+ ActionContext {
102+ id: localContext
103+ active: page.active
104+ objectName: page.objectName + "Context"
105+ }
106+
107+ /*!
108+ \since Ubuntu.Components 1.3
109 The header property for this page. Setting this property will reparent the
110 header to the page and disable the \l MainView's application header.
111 \qml
112@@ -157,13 +172,13 @@
113 Deprecated: This configuration will be replaced by setting the \l header property.
114 */
115 readonly property alias head: headerConfig
116- Toolkit13.PageHeadConfiguration {
117+ PageHeadConfiguration {
118 id: headerConfig
119 title: page.title
120 flickable: page.flickable
121 }
122
123- Toolkit13.Object {
124+ Object {
125 id: internal
126
127 property Item previousHeader: null
128
129=== modified file 'src/Ubuntu/Components/Popups/1.3/Dialog.qml'
130--- src/Ubuntu/Components/Popups/1.3/Dialog.qml 2015-12-14 08:27:39 +0000
131+++ src/Ubuntu/Components/Popups/1.3/Dialog.qml 2016-01-12 14:26:24 +0000
132@@ -180,6 +180,13 @@
133 height: childrenRect.height + foreground.margins
134 onWidthChanged: updateChildrenWidths();
135
136+ // put the context into this component to save ActionContext lookup
137+ PopupContext {
138+ id: localContext
139+ objectName: dialog.objectName + "DialogContext"
140+ active: foreground.visible
141+ }
142+
143 Label {
144 horizontalAlignment: Text.AlignHCenter
145 text: dialog.title
146
147=== modified file 'src/Ubuntu/Components/Popups/1.3/Popover.qml'
148--- src/Ubuntu/Components/Popups/1.3/Popover.qml 2015-12-09 12:34:18 +0000
149+++ src/Ubuntu/Components/Popups/1.3/Popover.qml 2016-01-12 14:26:24 +0000
150@@ -227,6 +227,14 @@
151 right: parent.right
152 }
153 height: childrenRect.height
154+
155+ // put the PopupContext inside the container to save one step
156+ // in the context lookup
157+ PopupContext {
158+ id: popupContext
159+ objectName: popover.objectName + "PopupContext"
160+ active: foreground.visible
161+ }
162 }
163
164 onWidthChanged: internal.updatePosition()
165
166=== modified file 'src/Ubuntu/Components/plugin/adapters/actionsproxy_p.cpp'
167--- src/Ubuntu/Components/plugin/adapters/actionsproxy_p.cpp 2015-09-01 10:49:47 +0000
168+++ src/Ubuntu/Components/plugin/adapters/actionsproxy_p.cpp 2016-01-12 14:26:24 +0000
169@@ -19,29 +19,23 @@
170
171 #include <QDebug>
172
173+Q_LOGGING_CATEGORY(ucActionProxy, "ubuntu.components.ActionProxy", QtMsgType::QtWarningMsg)
174+
175+#define AP_TRACE(params) qCDebug(ucActionProxy) << params
176+
177 ActionProxy::ActionProxy()
178- : QObject(0)
179- , globalContext(new UCActionContext)
180+ : globalContext(new UCActionContext)
181 {
182 // for testing purposes
183 globalContext->setObjectName("GlobalActionContext");
184 }
185 ActionProxy::~ActionProxy()
186 {
187- // if there is still an active context clear it
188- if (!m_activeContext.isNull()) {
189- m_activeContext->setActive(false);
190- }
191 // clear context explicitly, as global context is not connected to
192 clearContextActions(globalContext);
193 delete globalContext;
194 }
195
196-UCActionContext *ActionProxy::currentContext()
197-{
198- return instance().m_activeContext;
199-}
200-
201 const QSet<UCActionContext*> &ActionProxy::localContexts()
202 {
203 return instance().m_localContexts;
204@@ -67,8 +61,7 @@
205 return;
206 }
207 instance().m_localContexts.insert(context);
208- // watch context activation changes
209- instance().watchContextActivation(context, true);
210+ AP_TRACE("ADD CONTEXT" << context);
211 }
212 // Remove a local context. If the context was active, removes the actions from the system.
213 void ActionProxy::removeContext(UCActionContext *context)
214@@ -78,60 +71,77 @@
215 }
216 // make sure the context is deactivated
217 context->setActive(false);
218- instance().watchContextActivation(context, false);
219 instance().m_localContexts.remove(context);
220-}
221-
222-// toggles context activation watching for a given context
223-void ActionProxy::watchContextActivation(UCActionContext *context, bool watch)
224-{
225- if (!context) {
226- return;
227- }
228- if (watch) {
229- // connect to action proxy
230- QObject::connect(context, SIGNAL(activeChanged()),
231- this, SLOT(handleContextActivation()),
232- Qt::DirectConnection);
233- } else {
234- // disconnect
235- QObject::disconnect(context, SIGNAL(activeChanged()),
236- this, SLOT(handleContextActivation()));
237- }
238-}
239-
240-// handles the local context activation
241-void ActionProxy::handleContextActivation()
242-{
243- // sender is the context changing activation
244- UCActionContext *context = qobject_cast<UCActionContext*>(sender());
245- if (!context) {
246- return;
247- }
248- // deactivate the previous context if any
249- if (!m_activeContext.isNull()) {
250- if (!context->active()) {
251- // the slot has been called due to the previous active deactivation,
252- // so perform system cleanup
253- clearContextActions(m_activeContext);
254- m_activeContext->markActionsPublished(false);
255- // finally clear the context and leave
256- m_activeContext.clear();
257- return;
258- } else {
259- // deactivate previous actiev context, this will cause the slot to
260- // be called with active = false within this call context
261- m_activeContext->setActive(false);
262- }
263- }
264+ AP_TRACE("REMOVE CONTEXT FROM REGISTRY" << context);
265+}
266+
267+// publishes/removes context actions on activation/deactivation
268+void ActionProxy::activateContext(UCActionContext *context)
269+{
270+ if (!context) {
271+ return;
272+ }
273+
274+ // if a context to be activated is a popup one, we must deactivate all other ones
275+ // and then activate this
276 if (context->active()) {
277 // publish the context's actions to the system
278- publishContextActions(context);
279+ instance().publishContextActions(context);
280 context->markActionsPublished(true);
281- // and finally set it as active
282- m_activeContext = context;
283- }
284-}
285+
286+ if (context->isPopup()) {
287+ instance().addPopupContext(static_cast<UCPopupContext*>(context));
288+ } else {
289+ AP_TRACE("ACTIVATE CONTEXT" << context);
290+ }
291+ } else {
292+ // remove actions from the system
293+ instance().clearContextActions(context);
294+ context->markActionsPublished(false);
295+
296+ if (context->isPopup()) {
297+ instance().removePopupContext(static_cast<UCPopupContext*>(context));
298+ } else {
299+ AP_TRACE("DEACTIVATE CONTEXT" << context);
300+ }
301+ }
302+}
303+
304+void ActionProxy::addPopupContext(UCPopupContext *context)
305+{
306+ // deactivate last context and append
307+ UCPopupContext *lastActive = m_popupContexts.isEmpty() ?
308+ Q_NULLPTR : m_popupContexts.top();
309+ if (lastActive) {
310+ lastActive->setEffectiveActive(false);
311+ AP_TRACE("DEACTIVATE POPUPCONTEXT" << lastActive);
312+ }
313+ m_popupContexts.push(context);
314+ AP_TRACE("ACTIVATE POPUPCONTEXT" << context);
315+}
316+
317+void ActionProxy::removePopupContext(UCPopupContext *context)
318+{
319+ UCPopupContext *last = m_popupContexts.isEmpty() ?
320+ Q_NULLPTR : m_popupContexts.top();
321+
322+ if (last == context) {
323+ // we are about to remove the last one
324+ AP_TRACE("DEACTIVATE POPUPCONTEXT" << last);
325+ m_popupContexts.pop();
326+ // and then re-activate the second last one
327+ last = m_popupContexts.isEmpty() ? Q_NULLPTR : m_popupContexts.top();
328+ if (last) {
329+ AP_TRACE("REACTIVATE POPUPCONTEXT" << last);
330+ last->setEffectiveActive(true);
331+ }
332+ } else {
333+ // we simply remove the context and leave
334+ AP_TRACE("REMOVE POPUPCONTEXT" << context);
335+ m_popupContexts.removeAll(context);
336+ }
337+}
338+
339 // empty functions for context activation/deactivation, connect to HUD
340 void ActionProxy::clearContextActions(UCActionContext *context)
341 {
342
343=== modified file 'src/Ubuntu/Components/plugin/adapters/actionsproxy_p.h'
344--- src/Ubuntu/Components/plugin/adapters/actionsproxy_p.h 2015-09-01 10:49:47 +0000
345+++ src/Ubuntu/Components/plugin/adapters/actionsproxy_p.h 2016-01-12 14:26:24 +0000
346@@ -23,9 +23,9 @@
347 #include "ucaction.h"
348
349 class UCActionContext;
350-class ActionProxy : public QObject
351+class UCPopupContext;
352+class ActionProxy
353 {
354- Q_OBJECT
355 public:
356
357 ~ActionProxy();
358@@ -37,24 +37,25 @@
359
360 UCActionContext *globalContext;
361
362- static UCActionContext *currentContext();
363 static const QSet<UCActionContext*> &localContexts();
364 static void publishGlobalContext();
365 static void addContext(UCActionContext *context);
366 static void removeContext(UCActionContext *context);
367+ static void activateContext(UCActionContext *context);
368
369 protected:
370 ActionProxy();
371
372-protected Q_SLOTS:
373- void watchContextActivation(UCActionContext *context, bool watch);
374- void handleContextActivation();
375+protected:
376 virtual void clearContextActions(UCActionContext *context);
377 virtual void publishContextActions(UCActionContext *context);
378
379 private:
380 QSet<UCActionContext*> m_localContexts;
381- QPointer<UCActionContext> m_activeContext;
382+ QStack<UCPopupContext*> m_popupContexts;
383+
384+ void addPopupContext(UCPopupContext *context);
385+ void removePopupContext(UCPopupContext *context);
386 };
387
388 #endif // ACTIONSPROXY_P_H
389
390=== modified file 'src/Ubuntu/Components/plugin/plugin.cpp'
391--- src/Ubuntu/Components/plugin/plugin.cpp 2015-12-17 15:23:26 +0000
392+++ src/Ubuntu/Components/plugin/plugin.cpp 2016-01-12 14:26:24 +0000
393@@ -263,6 +263,7 @@
394 qmlRegisterType<UCBottomEdge>(uri, 1, 3, "BottomEdge");
395 qmlRegisterType<UCBottomEdgeRegion>(uri, 1, 3, "BottomEdgeRegion");
396 qmlRegisterType<UCPageTreeNode>(uri, 1, 3, "PageTreeNode");
397+ qmlRegisterType<UCPopupContext>(uri, 1, 3, "PopupContext");
398 }
399
400 void UbuntuComponentsPlugin::initializeEngine(QQmlEngine *engine, const char *uri)
401
402=== modified file 'src/Ubuntu/Components/plugin/ucaction.cpp'
403--- src/Ubuntu/Components/plugin/ucaction.cpp 2015-12-18 15:20:48 +0000
404+++ src/Ubuntu/Components/plugin/ucaction.cpp 2016-01-12 14:26:24 +0000
405@@ -16,6 +16,7 @@
406
407 #include "ucaction.h"
408 #include "quickutils.h"
409+#include "ucactioncontext.h"
410
411 #include <QtDebug>
412 #include <QtQml/QQmlInfo>
413@@ -23,12 +24,16 @@
414 #include <QtQuick/qquickwindow.h>
415 #include <private/qguiapplication_p.h>
416
417+Q_LOGGING_CATEGORY(ucAction, "ubuntu.components.Action", QtMsgType::QtWarningMsg)
418+
419+#define ACT_TRACE(params) qCDebug(ucAction) << params
420+
421 bool shortcutContextMatcher(QObject* object, Qt::ShortcutContext context)
422 {
423 UCAction* action = static_cast<UCAction*>(object);
424- // Can't access member here because it's not public
425- if (!action->property("enabled").toBool())
426+ if (!action->isEnabled()) {
427 return false;
428+ }
429
430 switch (context) {
431 case Qt::ApplicationShortcut:
432@@ -37,10 +42,39 @@
433 QObject* window = object;
434 while (window && !window->isWindowType()) {
435 window = window->parent();
436- if (QQuickItem* item = qobject_cast<QQuickItem*>(window))
437+ if (QQuickItem* item = qobject_cast<QQuickItem*>(window)) {
438 window = item->window();
439- }
440- return window && window == QGuiApplication::focusWindow();
441+ }
442+ }
443+ bool activatable = window && window == QGuiApplication::focusWindow();
444+
445+ if (activatable) {
446+ // is the last action owner item in an active context?
447+ QQuickItem *pl = action->lastOwningItem();
448+ activatable = false;
449+ while (pl) {
450+ UCActionContextAttached *attached = static_cast<UCActionContextAttached*>(
451+ qmlAttachedPropertiesObject<UCActionContext>(pl, false));
452+ if (attached) {
453+ activatable = attached->context()->active();
454+ if (!activatable) {
455+ ACT_TRACE(action << "Inactive context found" << attached->context());
456+ break;
457+ }
458+ }
459+ pl = pl->parentItem();
460+ }
461+ if (!activatable) {
462+ // check if the action is in an active context
463+ UCActionContext *context = qobject_cast<UCActionContext*>(action->parent());
464+ activatable = context && context->active();
465+ }
466+ }
467+ if (activatable) {
468+ ACT_TRACE("SELECTED ACTION" << action);
469+ }
470+
471+ return activatable;
472 }
473 default: break;
474 }
475@@ -152,6 +186,7 @@
476 mnemonic = mnemonic.toLower();
477 mnemonicIndex = m_text.indexOf(mnemonic);
478 }
479+ ACT_TRACE("MNEM" << mnemonic);
480 QString displayText(m_text);
481 // FIXME: we need QInputDeviceInfo to detect the keyboard attechment
482 // https://bugs.launchpad.net/ubuntu/+source/ubuntu-ui-toolkit/+bug/1276808
483@@ -192,6 +227,7 @@
484 m_mnemonic = sequence;
485
486 if (!m_mnemonic.isEmpty()) {
487+ ACT_TRACE("MNEMONIC SET" << m_mnemonic.toString());
488 Qt::ShortcutContext context = Qt::WindowShortcut;
489 QGuiApplicationPrivate::instance()->shortcutMap.addShortcut(this, m_mnemonic, context, shortcutContextMatcher);
490 }
491@@ -356,10 +392,12 @@
492 }
493
494 QKeySequence sequenceFromVariant(const QVariant& variant) {
495- if (variant.type() == QVariant::Int)
496+ if (variant.type() == QVariant::Int) {
497 return static_cast<QKeySequence::StandardKey>(variant.toInt());
498- if (variant.type() == QVariant::String)
499+ }
500+ if (variant.type() == QVariant::String) {
501 return QKeySequence::fromString(variant.toString());
502+ }
503 return QKeySequence();
504 }
505
506@@ -376,10 +414,12 @@
507 QGuiApplicationPrivate::instance()->shortcutMap.removeShortcut(0, this, sequenceFromVariant(m_shortcut));
508
509 QKeySequence sequence(sequenceFromVariant(shortcut));
510- if (!sequence.toString().isEmpty())
511+ if (!sequence.isEmpty()) {
512+ ACT_TRACE("ADD SHORTCUT" << sequence.toString());
513 QGuiApplicationPrivate::instance()->shortcutMap.addShortcut(this, sequence, Qt::WindowShortcut, shortcutContextMatcher);
514- else
515+ } else {
516 qmlInfo(this) << "Invalid shortcut: " << shortcut.toString();
517+ }
518
519 m_shortcut = shortcut;
520 Q_EMIT shortcutChanged();
521@@ -399,6 +439,8 @@
522 if (event->type() != QEvent::Shortcut)
523 return false;
524
525+ // when we reach this point, we can be sure the Action is used
526+ // by a component belonging to an active ActionContext.
527 QShortcutEvent *shortcut_event(static_cast<QShortcutEvent*>(event));
528 if (shortcut_event->isAmbiguous()) {
529 qmlInfo(this) << "Ambiguous shortcut: " << shortcut_event->key().toString();
530@@ -437,3 +479,17 @@
531 Q_EMIT triggered(value);
532 }
533 }
534+
535+void UCAction::addOwningItem(QQuickItem *item)
536+{
537+ if (!m_owningItems.contains(item)) {
538+ m_owningItems.append(item);
539+ ACT_TRACE("ADD ACTION OWNER" << item->objectName() << "TO" << this);
540+ }
541+}
542+
543+void UCAction::removeOwningItem(QQuickItem *item)
544+{
545+ m_owningItems.removeOne(item);
546+ ACT_TRACE("REMOVE ACTION OWNER" << item->objectName() << "FROM" << this);
547+}
548
549=== modified file 'src/Ubuntu/Components/plugin/ucaction.h'
550--- src/Ubuntu/Components/plugin/ucaction.h 2015-12-18 12:42:58 +0000
551+++ src/Ubuntu/Components/plugin/ucaction.h 2016-01-12 14:26:24 +0000
552@@ -21,6 +21,9 @@
553 #include <QtCore/QVariant>
554 #include <QtCore/QUrl>
555 #include <QtGui/QKeySequence>
556+#include <QtQml>
557+#include <QtQml/QQmlListProperty>
558+#include <QtQml/private/qpodvector_p.h>
559
560 // the function detects whether QML has an overridden trigger() slot available
561 // and invokes the one with the appropriate signature
562@@ -45,6 +48,8 @@
563 }
564
565 class QQmlComponent;
566+class QQuickItem;
567+class UCActionAttached;
568 class UCAction : public QObject
569 {
570 Q_OBJECT
571@@ -83,6 +88,17 @@
572 {
573 return m_published;
574 }
575+ inline bool isEnabled() const
576+ {
577+ return m_enabled;
578+ }
579+ inline QQuickItem *lastOwningItem() const
580+ {
581+ return m_owningItems.count() > 0 ?
582+ m_owningItems.at(m_owningItems.count() - 1) : Q_NULLPTR;
583+ }
584+ void addOwningItem(QQuickItem *item);
585+ void removeOwningItem(QQuickItem *item);
586
587 void setName(const QString &name);
588 QString text();
589@@ -111,6 +127,7 @@
590 void trigger(const QVariant &value = QVariant());
591
592 private:
593+ QPODVector<QQuickItem*, 4> m_owningItems;
594 QString m_name;
595 QString m_text;
596 QString m_iconName;
597@@ -139,5 +156,6 @@
598 bool event(QEvent *event);
599 void onKeyboardAttached();
600 };
601+QML_DECLARE_TYPE(UCAction)
602
603 #endif // UCACTION_H
604
605=== modified file 'src/Ubuntu/Components/plugin/ucactioncontext.cpp'
606--- src/Ubuntu/Components/plugin/ucactioncontext.cpp 2015-09-01 10:49:47 +0000
607+++ src/Ubuntu/Components/plugin/ucactioncontext.cpp 2016-01-12 14:26:24 +0000
608@@ -17,6 +17,18 @@
609 #include "ucactioncontext.h"
610 #include "ucaction.h"
611 #include "adapters/actionsproxy_p.h"
612+#include <QtQuick/QQuickItem>
613+
614+Q_LOGGING_CATEGORY(ucActionContext, "ubuntu.components.ActionContext", QtMsgType::QtWarningMsg)
615+
616+#define CONTEXT_TRACE(params) qCDebug(ucActionContext) << params
617+
618+UCActionContextAttached::UCActionContextAttached(QObject *owner)
619+ : QObject(owner)
620+ , m_owner(qobject_cast<QQuickItem*>(owner))
621+ , m_context(Q_NULLPTR)
622+{
623+}
624
625 /*!
626 * \qmltype ActionContext
627@@ -26,10 +38,72 @@
628 * \brief ActionContext groups actions together and by providing multiple contexts
629 * the developer is able to control the visibility of the actions. The \l ActionManager
630 * then exposes the actions from these different contexts.
631+ *
632+ * ActionContext drives the state of its \l actions. Shortcuts and mnemonics are
633+ * only registered if the context is active or if the action is assigned to an
634+ * \l ActionItem all of whose parent contexts are active. In the following
635+ * example the ActionContext drives the underlaying \c action1 and \c action2
636+ * shortcuts, and \c orphanAction will never trigger as it is neither enclosed
637+ * in an active context nor assigned to an ActionItem.
638+ * \qml
639+ * import QtQuick 2.4
640+ * import ubuntu.Componenst 1.3
641+ *
642+ * Rectangle {
643+ * id: root
644+ * width: units.gu(40)
645+ * height: units.gu(71)
646+ * ActionContext {
647+ * id: rootContext
648+ * active: true
649+ * actions: Action {
650+ * shortcut: 'Ctrl+A'
651+ * text: rootContext.active ? "Deactivate" : "Activate"
652+ * onTriggered: rootContext.active = !rootContext.active
653+ * }
654+ * }
655+ *
656+ * Action {
657+ * id: orphanAction
658+ * text: "Orphan"
659+ * shortcut: 'Ctrl+O'
660+ * onTriggered: console.log("This will not be called")
661+ * }
662+ *
663+ * Column {
664+ * Button {
665+ * text: rootContext.active ? "Deactivate" : "Activate"
666+ * onClicked: rootContext.active = !rootContext.active
667+ * }
668+ * Button {
669+ * action: Action {
670+ * id: action1
671+ * text: "F&irst Button"
672+ * onTriggered: console.log("First Button triggered")
673+ * }
674+ * }
675+ * Button {
676+ * action: Action {
677+ * id: action2
678+ * text: "S&econd Button"
679+ * shortcut: 'Ctrl+Alt+2'
680+ * onTriggered: console.log("Second Button triggered")
681+ * }
682+ * }
683+ * }
684+ * }
685+ * \endqml
686+ *
687+ * The toolkit assigns an ActionContext to each Page component, which is
688+ * activated/deactivated together with the Page itself, driving the shortcut
689+ * activations on the components and actions declared in the Page.
690+ * \sa PopupContext
691 */
692 UCActionContext::UCActionContext(QObject *parent)
693 : QObject(parent)
694 , m_active(false)
695+ , m_effectiveActive(true)
696+ , m_popup(false)
697 {
698 }
699 UCActionContext::~UCActionContext()
700@@ -37,10 +111,23 @@
701 ActionProxy::removeContext(this);
702 }
703
704-void UCActionContext::componentComplete()
705+UCActionContextAttached *UCActionContext::qmlAttachedProperties(QObject *owner)
706+{
707+ return new UCActionContextAttached(owner);
708+}
709+
710+void UCActionContext::classBegin()
711 {
712 // add the context to the management
713 ActionProxy::addContext(this);
714+ // make sure we attach to the parent
715+ UCActionContextAttached *attached = static_cast<UCActionContextAttached*>(
716+ qmlAttachedPropertiesObject<UCActionContext>(parent(), true));
717+ attached->m_context = this;
718+}
719+
720+void UCActionContext::componentComplete()
721+{
722 }
723
724 /*
725@@ -97,18 +184,16 @@
726 * whether or not the actions in a context are available to external components.
727 *
728 * The \l ActionManager monitors the active property of each of the local contexts
729- * that has been added to it. There can be only one active local context at a time.
730- * When one of the local contexts sets itself active the manager will notice this,
731- * export the actions from that given context and set the previously active local
732- * context as inactive. This way setting active to true on a local context is
733- * sufficient to manage the active local context of the manager and no additional
734- * calls are necessary to manually inactivate the other contexts.
735+ * that has been added to it. There can be more than one local context active at a.
736+ * time. When a local context is set active the manager will notice this and will
737+ * export the actions from the context.
738+ * \note An Action declared to a component falling under an item that is a child of
739+ * an inactive ActiveContext can be triggered manually using the mouse or connections.
740 */
741 bool UCActionContext::active()
742 {
743- return m_active;
744+ return m_active && m_effectiveActive;
745 }
746-
747 void UCActionContext::setActive(bool active)
748 {
749 if (m_active == active) {
750@@ -118,7 +203,26 @@
751 if (!active && (ActionProxy::instance().globalContext == this)) {
752 return;
753 }
754+ CONTEXT_TRACE("ACTIVATE CONTEXT" << this << active);
755+
756 m_active = active;
757+ ActionProxy::activateContext(this);
758+ Q_EMIT activeChanged();
759+}
760+
761+// similar to setActive() but does not alter the actions from the proxy
762+void UCActionContext::setEffectiveActive(bool active)
763+{
764+ if (m_effectiveActive == active) {
765+ return;
766+ }
767+ // skip deactivation for global context
768+ if (!active && (ActionProxy::instance().globalContext == this)) {
769+ return;
770+ }
771+ CONTEXT_TRACE("EFECTIVE ACTIVATE CONTEXT" << this << active);
772+
773+ m_effectiveActive = active;
774 Q_EMIT activeChanged();
775 }
776
777@@ -147,3 +251,28 @@
778 }
779 m_actions.remove(action);
780 }
781+
782+
783+/*!
784+ * \qmltype PopupContext
785+ * \instantiates UCPopupContext
786+ * \inqmlmodule Ubuntu.Components 1.3
787+ * \since Ubuntu.Components 1.3
788+ * \inherits ActionContext
789+ * \ingroup ubuntu
790+ * \brief A special ActionContext used in Dialogs and Popups.
791+ *
792+ * A PopupContext is similar to the ActionContext, with the only difference being
793+ * that there can be only one PopupContext active at a time in an application.
794+ * A PopupContext can have several active ActionContext children declared, however
795+ * when deactivated all child contexts will be deactivated as well, and no Action
796+ * declared in these contexts will be available through shortcuts.
797+ *
798+ * The toolkit provides this kind of contexts in MainView, Popup and Dialog. It is
799+ * highly recommended for applications to have a PopupContext defined in their rootItem.
800+ */
801+UCPopupContext::UCPopupContext(QObject *parent)
802+ : UCActionContext(parent)
803+{
804+ m_popup = true;
805+}
806
807=== modified file 'src/Ubuntu/Components/plugin/ucactioncontext.h'
808--- src/Ubuntu/Components/plugin/ucactioncontext.h 2015-09-01 10:49:47 +0000
809+++ src/Ubuntu/Components/plugin/ucactioncontext.h 2016-01-12 14:26:24 +0000
810@@ -24,6 +24,7 @@
811 #include <QtQml>
812
813 class UCAction;
814+class UCActionContextAttached;
815 class UCActionContext : public QObject, public QQmlParserStatus
816 {
817 Q_OBJECT
818@@ -35,14 +36,21 @@
819 explicit UCActionContext(QObject *parent = 0);
820 ~UCActionContext();
821
822- void classBegin(){}
823+ static UCActionContextAttached *qmlAttachedProperties(QObject *owner);
824+
825+ void classBegin();
826 void componentComplete();
827 void markActionsPublished(bool mark);
828+ bool isPopup() const
829+ {
830+ return m_popup;
831+ }
832
833 QQmlListProperty<UCAction> actions();
834
835 bool active();
836 void setActive(bool active);
837+ void setEffectiveActive(bool active);
838
839 Q_SIGNALS:
840 void activeChanged();
841@@ -51,9 +59,13 @@
842 void addAction(UCAction *action);
843 void removeAction(UCAction *action);
844
845-private:
846- bool m_active;
847+protected:
848 QSet<UCAction*> m_actions;
849+ bool m_active:1;
850+ bool m_effectiveActive:1;
851+ // declare popup flag within ActionContext to avoid unnecessary object-casting
852+ // to detect whether a context is a popup or normal context.
853+ bool m_popup:1;
854 friend class UCActionManager;
855
856 static void append(QQmlListProperty<UCAction> *list, UCAction *action);
857@@ -61,6 +73,36 @@
858 static int count(QQmlListProperty<UCAction> *list);
859 };
860
861+class UCPopupContext : public UCActionContext
862+{
863+ Q_OBJECT
864+public:
865+ explicit UCPopupContext(QObject *parent = 0);
866+};
867+
868+class QQuickItem;
869+class UCActionContextAttached : public QObject
870+{
871+ Q_OBJECT
872+public:
873+ explicit UCActionContextAttached(QObject *owner);
874+
875+ inline QQuickItem *owner() const
876+ {
877+ return m_owner;
878+ }
879+ inline UCActionContext *context() const
880+ {
881+ return m_context;
882+ }
883+
884+private:
885+ QQuickItem *m_owner;
886+ UCActionContext *m_context;
887+ friend class UCActionContext;
888+};
889+
890 QML_DECLARE_TYPE(UCActionContext)
891+QML_DECLARE_TYPEINFO(UCActionContext, QML_HAS_ATTACHED_PROPERTIES)
892
893 #endif // UCACTIONCONTEXT_H
894
895=== modified file 'src/Ubuntu/Components/plugin/ucactionitem.cpp'
896--- src/Ubuntu/Components/plugin/ucactionitem.cpp 2015-12-18 12:42:58 +0000
897+++ src/Ubuntu/Components/plugin/ucactionitem.cpp 2016-01-12 14:26:24 +0000
898@@ -139,6 +139,7 @@
899 {
900 Q_Q(UCActionItem);
901 if (attach) {
902+ action->addOwningItem(q);
903 QObject::connect(q, SIGNAL(triggered(QVariant)),
904 q, SLOT(_q_invokeActionTrigger(QVariant)), Qt::DirectConnection);
905 if (!(flags & CustomVisible)) {
906@@ -162,6 +163,7 @@
907 q, &UCActionItem::iconNameChanged, Qt::DirectConnection);
908 }
909 } else {
910+ action->removeOwningItem(q);
911 QObject::disconnect(q, SIGNAL(triggered(QVariant)),
912 q, SLOT(_q_invokeActionTrigger(QVariant)));
913 if (!(flags & CustomVisible)) {
914@@ -228,7 +230,7 @@
915 if (d->flags & UCActionItemPrivate::CustomText) {
916 return d->text;
917 }
918- return d->action ? d->action->m_text : QString();
919+ return d->action ? d->action->text() : QString();
920 }
921 void UCActionItem::setText(const QString &text)
922 {
923
924=== modified file 'src/Ubuntu/Components/plugin/ucbottomedge_p.h'
925--- src/Ubuntu/Components/plugin/ucbottomedge_p.h 2015-12-11 12:10:54 +0000
926+++ src/Ubuntu/Components/plugin/ucbottomedge_p.h 2016-01-12 14:26:24 +0000
927@@ -109,6 +109,5 @@
928 UCCollapseAction(QObject *parent = 0);
929 void activate();
930 };
931-Q_DECLARE_METATYPE(QQmlListProperty<UCAction>)
932
933 #endif // UCBOTTOMEDGE_P_H
934
935=== modified file 'src/Ubuntu/Components/plugin/uclistitem.cpp'
936--- src/Ubuntu/Components/plugin/uclistitem.cpp 2015-12-15 15:58:54 +0000
937+++ src/Ubuntu/Components/plugin/uclistitem.cpp 2016-01-12 14:26:24 +0000
938@@ -1745,10 +1745,16 @@
939 if (mainAction == action) {
940 return;
941 }
942+ if (mainAction) {
943+ mainAction->removeOwningItem(q);
944+ }
945 mainAction = action;
946- if (mainAction && (mainAction->m_parameterType == UCAction::None)) {
947- // call setProperty to invoke notify signal
948- mainAction->setProperty("parameterType", UCAction::Integer);
949+ if (mainAction) {
950+ mainAction->addOwningItem(q);
951+ if (mainAction->m_parameterType == UCAction::None) {
952+ // call setProperty to invoke notify signal
953+ mainAction->setProperty("parameterType", UCAction::Integer);
954+ }
955 }
956 Q_EMIT q->actionChanged();
957 }
958
959=== modified file 'tests/unit/tst_components/tst_action.qml'
960--- tests/unit/tst_components/tst_action.qml 2015-12-12 07:22:08 +0000
961+++ tests/unit/tst_components/tst_action.qml 2016-01-12 14:26:24 +0000
962@@ -37,6 +37,8 @@
963 function cleanup() {
964 triggeredSignalSpy.target = action;
965 triggeredSignalSpy.clear();
966+ context1.active = false;
967+ context2.active = false;
968 }
969
970 function initTestCase() {
971@@ -126,15 +128,21 @@
972
973 function test_activate_contexts_data() {
974 return [
975- {tag: "Activate context1", active: context1, inactive: context2},
976- {tag: "Activate context2", active: context2, inactive: context1},
977- {tag: "Activate context1 again", active: context1, inactive: context2},
978+ {tag: "Activate context1", active: [context1], inactive: [context2]},
979+ {tag: "Activate context2", active: [context2], inactive: [context1]},
980+ {tag: "Activate context1, context2", active: [context1, context2], inactive: []},
981 ];
982 }
983 function test_activate_contexts(data) {
984- data.active.active = true;
985- verify(data.active.active, "Context activation error");
986- verify(!data.inactive.active, "Context deactivation error");
987+ for (var i = 0; i < data.active.length; i++) {
988+ data.active[i].active = true;
989+ }
990+ for (var i = 0; i < data.active.length; i++) {
991+ verify(data.active[i].active, "Context activation error");
992+ }
993+ for (var i = 0; i < data.inactive.length; i++) {
994+ verify(!data.inactive[i].active, "Context deactivation error");
995+ }
996 }
997
998 function test_overloaded_action_trigger_data() {
999
1000=== added file 'tests/unit_x11/tst_components/tst_contextual_actions.qml'
1001--- tests/unit_x11/tst_components/tst_contextual_actions.qml 1970-01-01 00:00:00 +0000
1002+++ tests/unit_x11/tst_components/tst_contextual_actions.qml 2016-01-12 14:26:24 +0000
1003@@ -0,0 +1,326 @@
1004+/*
1005+ * Copyright 2015 Canonical Ltd.
1006+ *
1007+ * This program is free software; you can redistribute it and/or modify
1008+ * it under the terms of the GNU Lesser General Public License as published by
1009+ * the Free Software Foundation; version 3.
1010+ *
1011+ * This program is distributed in the hope that it will be useful,
1012+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
1013+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1014+ * GNU Lesser General Public License for more details.
1015+ *
1016+ * You should have received a copy of the GNU Lesser General Public License
1017+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
1018+ */
1019+
1020+import QtQuick 2.4
1021+import QtTest 1.0
1022+import Ubuntu.Test 1.0
1023+import Ubuntu.Components 1.3
1024+import Ubuntu.Components.Popups 1.3
1025+
1026+Item {
1027+ width: units.gu(40)
1028+ height: units.gu(71)
1029+
1030+ Loader {
1031+ id: testLoader
1032+ anchors.fill: parent
1033+ }
1034+
1035+ Component {
1036+ id: inactiveActionContextInChain
1037+
1038+ Item {
1039+ anchors.fill: parent
1040+ ActionContext {
1041+ active: true
1042+ }
1043+ Item {
1044+ anchors.fill: parent
1045+ ActionContext {
1046+ objectName: "testContext"
1047+ active: true
1048+ }
1049+
1050+ ActionItem {
1051+ objectName: "testActionItem"
1052+ action: Action {
1053+ text: "Test"
1054+ shortcut: 'Ctrl+T'
1055+ }
1056+ }
1057+ }
1058+ }
1059+ }
1060+
1061+ Component {
1062+ id: ambiguiousShortcutsInSameContext
1063+ Item {
1064+ ActionContext {
1065+ active: true
1066+ }
1067+ Column {
1068+ ActionItem {
1069+ action: Action {
1070+ shortcut: "Ctrl+T"
1071+ }
1072+ }
1073+ ActionItem {
1074+ action: Action {
1075+ shortcut: "Ctrl+T"
1076+ }
1077+ }
1078+ }
1079+ }
1080+ }
1081+
1082+ Component {
1083+ id: ambiguiousShortcutsInDifferentContexts
1084+ Item {
1085+ id: root
1086+ anchors.fill: parent
1087+ ActionContext {
1088+ active: true
1089+ }
1090+ Row {
1091+ Item {
1092+ width: root.width / 2
1093+ height: root.height
1094+ ActionContext {
1095+ active: true
1096+ }
1097+ ActionItem {
1098+ action: Action {
1099+ shortcut: "Ctrl+T"
1100+ }
1101+ }
1102+ }
1103+ Item {
1104+ width: root.width / 2
1105+ height: root.height
1106+ ActionContext {
1107+ active: true
1108+ }
1109+ ActionItem {
1110+ action: Action {
1111+ shortcut: "Ctrl+T"
1112+ }
1113+ }
1114+ }
1115+ }
1116+ }
1117+ }
1118+
1119+ Component {
1120+ id: onePopupContextActive
1121+ Item {
1122+ PopupContext {
1123+ objectName: "popup1"
1124+ active: true
1125+ }
1126+ PopupContext {
1127+ objectName: "popup2"
1128+ }
1129+ PopupContext {
1130+ objectName: "popup3"
1131+ }
1132+ }
1133+ }
1134+
1135+ Component {
1136+ id: popupTest
1137+ MainView {
1138+ id: main
1139+ anchors.fill: parent
1140+ applicationName: "testApp"
1141+
1142+ property Popover popover: null
1143+
1144+ Button {
1145+ id: button
1146+ objectName: "mainButton"
1147+ anchors.centerIn: parent
1148+ action: Action {
1149+ objectName: "mainAction"
1150+ text: "Test &button"
1151+ shortcut: "Ctrl+T"
1152+ }
1153+ onClicked: main.popover = PopupUtils.open(popup, button)
1154+ }
1155+
1156+ Component {
1157+ id: popup
1158+ Popover {
1159+ contentWidth: units.gu(30)
1160+ contentHeight: units.gu(30)
1161+ Button {
1162+ objectName: "popupButton"
1163+ action: Action {
1164+ objectName: "popupAction"
1165+ text: "Test &button"
1166+ shortcut: "Ctrl+T"
1167+ }
1168+ }
1169+ }
1170+ }
1171+ }
1172+ }
1173+
1174+ Component {
1175+ id: dialogTest
1176+ MainView {
1177+ id: main
1178+ anchors.fill: parent
1179+ applicationName: "testApp"
1180+
1181+ property Dialog popover: null
1182+
1183+ Button {
1184+ id: button
1185+ objectName: "mainButton"
1186+ anchors.centerIn: parent
1187+ action: Action {
1188+ objectName: "mainAction"
1189+ text: "Test &button"
1190+ shortcut: "Ctrl+T"
1191+ }
1192+ onClicked: main.popover = PopupUtils.open(dialog)
1193+ }
1194+
1195+ Component {
1196+ id: dialog
1197+ Dialog {
1198+ contentWidth: units.gu(30)
1199+ contentHeight: units.gu(30)
1200+ Button {
1201+ objectName: "popupButton"
1202+ action: Action {
1203+ objectName: "popupAction"
1204+ text: "Test &button"
1205+ shortcut: "Ctrl+T"
1206+ }
1207+ }
1208+ }
1209+ }
1210+ }
1211+ }
1212+
1213+ UbuntuTestCase {
1214+ name: "ContextualActions"
1215+ when: windowShown
1216+
1217+ SignalSpy {
1218+ id: triggeredSpy
1219+ signalName: "triggered"
1220+ }
1221+
1222+ function createTest(component) {
1223+ testLoader.sourceComponent = component;
1224+ tryCompareFunction(function() { return testLoader.item != null }, true, 1000);
1225+ waitForRendering(testLoader.item);
1226+ wait(200)
1227+ return testLoader.item;
1228+ }
1229+
1230+ function cleanup() {
1231+ testLoader.sourceComponent = null;
1232+ triggeredSpy.target = null;
1233+ triggeredSpy.clear();
1234+ wait(200);
1235+ }
1236+
1237+ function test_inactive_actioncontext_in_chain() {
1238+ var item = createTest(inactiveActionContextInChain);
1239+ var testContext = findInvisibleChild(item, "testContext");
1240+ verify(testContext);
1241+ var testActionItem = findInvisibleChild(item, "testActionItem");
1242+ verify(testActionItem);
1243+
1244+ testContext.active = true;
1245+ triggeredSpy.target = testActionItem.action;
1246+ keyPress(Qt.Key_T, Qt.ControlModifier);
1247+ triggeredSpy.wait(200);
1248+
1249+ testContext.active = false;
1250+ triggeredSpy.clear();
1251+ keyPress(Qt.Key_T, Qt.ControlModifier);
1252+ expectFailContinue("", "No trigger when a context is inactive");
1253+ triggeredSpy.wait(200);
1254+ }
1255+
1256+ function test_ambiguous_actions_when_multiple_contexts_active_data() {
1257+ return [
1258+ {tag: "within same ActionContext", test: ambiguiousShortcutsInSameContext, message: warningFormat(66, 29, "QML Action: Ambiguous shortcut: Ctrl+T")},
1259+ {tag: "within different ActionContexts", test: ambiguiousShortcutsInDifferentContexts, message: warningFormat(107, 33, "QML Action: Ambiguous shortcut: Ctrl+T")},
1260+ ];
1261+ }
1262+ function test_ambiguous_actions_when_multiple_contexts_active(data) {
1263+ var test = createTest(data.test);
1264+ ignoreWarning(data.message);
1265+ keyClick(Qt.Key_T, Qt.ControlModifier);
1266+ }
1267+
1268+ function test_one_popup_context_active() {
1269+ var test = createTest(onePopupContextActive);
1270+ var popup1 = findInvisibleChild(test, "popup1");
1271+ var popup2 = findInvisibleChild(test, "popup2");
1272+ var popup3 = findInvisibleChild(test, "popup3");
1273+ verify(popup1);
1274+ verify(popup2);
1275+ verify(popup3);
1276+ verify(popup1.active);
1277+ verify(!popup2.active);
1278+ verify(!popup3.active);
1279+
1280+ // activate popup2
1281+ popup2.active = true;
1282+ verify(!popup1.active);
1283+ verify(popup2.active);
1284+ verify(!popup3.active);
1285+
1286+ // activate popup2
1287+ popup3.active = true;
1288+ verify(!popup1.active);
1289+ verify(!popup2.active);
1290+ verify(popup3.active);
1291+
1292+ // deactivate popup3, popup2 should be re-activated
1293+ popup3.active = false;
1294+ verify(!popup1.active);
1295+ verify(popup2.active);
1296+ }
1297+
1298+ function test_popovers_data() {
1299+ return [
1300+ {tag: "Popup", component: popupTest},
1301+ {tag: "Dialog", component: dialogTest},
1302+ ];
1303+ }
1304+ function test_popovers(data) {
1305+ var test = createTest(data.component);
1306+
1307+ var mainButton = findChild(test, "mainButton");
1308+ verify(mainButton);
1309+ triggeredSpy.target = mainButton.action;
1310+ keyClick(Qt.Key_T, Qt.ControlModifier);
1311+ triggeredSpy.wait(200);
1312+ mouseClick(mainButton, centerOf(mainButton).x, centerOf(mainButton).y);
1313+ tryCompareFunction(function() { return test.popover != null;}, true, 1000);
1314+
1315+ // trigger the action
1316+ triggeredSpy.clear();
1317+ keyClick(Qt.Key_T, Qt.ControlModifier);
1318+ expectFailContinue(data.tag, "Popup is active now");
1319+ triggeredSpy.wait(200);
1320+
1321+ var actionItem = findChild(test.popover, "popupButton");
1322+ verify(actionItem);
1323+ triggeredSpy.target = actionItem.action;
1324+ triggeredSpy.clear();
1325+ keyClick(Qt.Key_B, Qt.AltModifier);
1326+ triggeredSpy.wait(200);
1327+ }
1328+ }
1329+}
1330
1331=== modified file 'tests/unit_x11/tst_components/tst_shortcuts.qml'
1332--- tests/unit_x11/tst_components/tst_shortcuts.qml 2015-12-18 15:20:48 +0000
1333+++ tests/unit_x11/tst_components/tst_shortcuts.qml 2016-01-12 14:26:24 +0000
1334@@ -24,12 +24,17 @@
1335 width: 400
1336 height: 600
1337
1338- Action {
1339- id: action
1340- }
1341- Action {
1342- id: other
1343- shortcut: 'Ctrl+G'
1344+ // actions must be either assigned to an active ActionContext or to an ActionItem to activate shortcuts
1345+ ActionContext {
1346+ id: context
1347+ active: true
1348+ Action {
1349+ id: action
1350+ }
1351+ Action {
1352+ id: other
1353+ shortcut: 'Ctrl+G'
1354+ }
1355 }
1356
1357 TestUtil {
1358@@ -45,6 +50,8 @@
1359 }
1360
1361 function init() {
1362+ context.active = true;
1363+ spy.target = action;
1364 }
1365 function cleanup() {
1366 spy.clear();
1367@@ -84,12 +91,12 @@
1368 ];
1369 }
1370 function test_shortcut_invalid(data) {
1371- ignoreQMLWarning(':27:5: QML Action: Invalid shortcut: ');
1372+ ignoreQMLWarning(':31:9: QML Action: Invalid shortcut: ');
1373 action.shortcut = data;
1374 }
1375
1376 function test_shortcut_duplicate() {
1377- ignoreQMLWarning(':30:5: QML Action: Ambiguous shortcut: Ctrl+G');
1378+ ignoreQMLWarning(':34:9: QML Action: Ambiguous shortcut: Ctrl+G');
1379 action.shortcut = other.shortcut;
1380 keyClick(Qt.Key_G, Qt.ControlModifier);
1381 }
1382@@ -161,5 +168,22 @@
1383 }
1384 textSpy.wait(200);
1385 }
1386+
1387+ function test_contextual_action_shortcut_data() {
1388+ return [
1389+ {tag: "Active context", active: true, xfail: false},
1390+ {tag: "Inactive context", active: false, xfail: true},
1391+ ];
1392+ }
1393+ function test_contextual_action_shortcut(data) {
1394+ context.active = data.active;
1395+ spy.target = other;
1396+ spy.clear();
1397+ keyClick(Qt.Key_G, Qt.ControlModifier);
1398+ if (data.xfail) {
1399+ expectFailContinue("", "No shortcut fires");
1400+ }
1401+ spy.wait(200);
1402+ }
1403 }
1404 }

Subscribers

People subscribed via source and target branches