Merge lp:~zsombi/ubuntu-ui-toolkit/contextual_actions into lp:ubuntu-ui-toolkit/staging
- contextual_actions
- Merge into staging
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 | ||||
Related bugs: |
|
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.
- 1789. By Zsombor Egri
-
fix ActionContext behavior
- 1790. By Zsombor Egri
-
API fixed
- 1791. By Zsombor Egri
-
rolling back unwanted changes
PS Jenkins bot (ps-jenkins) wrote : | # |
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:1791
http://
Executed test runs:
UNSTABLE: http://
UNSTABLE: http://
deb: http://
UNSTABLE: http://
Click here to trigger a rebuild:
http://
- 1792. By Zsombor Egri
-
register active contexts
- 1793. By Zsombor Egri
-
PopupContext in MainView, Popover and Dialog
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:1793
http://
Executed test runs:
UNSTABLE: http://
UNSTABLE: http://
deb: http://
UNSTABLE: http://
Click here to trigger a rebuild:
http://
- 1794. By Zsombor Egri
-
lightwaight ActionProxy; tests added
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:1794
http://
Executed test runs:
UNSTABLE: http://
SUCCESS: http://
deb: http://
UNSTABLE: http://
Click here to trigger a rebuild:
http://
Cris Dywan (kalikiana) wrote : | # |
+ bool activable = window && window == QGuiApplication
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_effectiveActi
effectivelyActive
- 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
Zsombor Egri (zsombi) wrote : | # |
> + bool activable = window && window == QGuiApplication
>
> 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.
>
> + 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_effectiveActi
>
> effectivelyActive
I followed QtQuick naming, they use effectiveVisible, effectiveLayout
- 1798. By Zsombor Egri
-
few review comments applied
PS Jenkins bot (ps-jenkins) wrote : | # |
PASSED: Continuous integration, rev:1797
http://
Executed test runs:
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
PS Jenkins bot (ps-jenkins) wrote : | # |
PASSED: Continuous integration, rev:1798
http://
Executed test runs:
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
ubuntu-sdk-build-bot (ubuntu-sdk-build-bot) : | # |
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:/
Executed test runs:
None: https:/
ubuntu-sdk-build-bot (ubuntu-sdk-build-bot) : | # |
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:/
Executed test runs:
None: https:/
Cris Dywan (kalikiana) wrote : | # |
> > * 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.
> 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_effectiveActi
> >
> > effectivelyActive
>
> I followed QtQuick naming, they use effectiveVisible, effectiveLa...
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
Cris Dywan (kalikiana) wrote : | # |
Thanks for the updates and the added test. Looking nice now.
PS Jenkins bot (ps-jenkins) wrote : | # |
PASSED: Continuous integration, rev:1800
http://
Executed test runs:
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
Preview Diff
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 | } |
FAILED: Continuous integration, rev:1790 jenkins. qa.ubuntu. com/job/ ubuntu- sdk-team- ubuntu- ui-toolkit- staging- ci/2677/ jenkins. qa.ubuntu. com/job/ ubuntu- sdk-team- ubuntu- ui-toolkit- staging- vivid-amd64- ci/1402 jenkins. qa.ubuntu. com/job/ ubuntu- sdk-team- ubuntu- ui-toolkit- staging- vivid-armhf- ci/1403 jenkins. qa.ubuntu. com/job/ ubuntu- sdk-team- ubuntu- ui-toolkit- staging- vivid-armhf- ci/1403/ artifact/ work/output/ *zip*/output. zip jenkins. qa.ubuntu. com/job/ ubuntu- sdk-team- ubuntu- ui-toolkit- staging- vivid-i386- ci/1401
http://
Executed test runs:
UNSTABLE: http://
UNSTABLE: http://
deb: http://
UNSTABLE: http://
Click here to trigger a rebuild: s-jenkins. ubuntu- ci:8080/ job/ubuntu- sdk-team- ubuntu- ui-toolkit- staging- ci/2677/ rebuild
http://