1 | // SPDX-FileCopyrightText: 2024 Carl Schwan <carl@carlschwan.eu> |
2 | // SPDX-License-Identifier: LGPL-2.1-or-later |
3 | |
4 | #include "pagestackattached.h" |
5 | |
6 | #include "formlayoutattached.h" |
7 | #include "loggingcategory.h" |
8 | |
9 | #include <QMetaObject> |
10 | #include <QQmlContext> |
11 | #include <QQmlEngine> |
12 | |
13 | using namespace Qt::StringLiterals; |
14 | |
15 | template<typename... Args> |
16 | bool callIfValid(QObject *object, const char *method, Args &&...args) |
17 | { |
18 | auto metaObject = object->metaObject(); |
19 | auto index = metaObject->indexOfMethod(method); |
20 | if (index != -1) { |
21 | auto method = metaObject->method(index); |
22 | return method.invoke(object, args...); |
23 | } |
24 | |
25 | return false; |
26 | } |
27 | |
28 | bool tryCall(QObject *object, QByteArrayView , QByteArrayView stackViewMethod, const QVariant &page, const QVariantMap &properties) |
29 | { |
30 | const auto metaObject = object->metaObject(); |
31 | |
32 | QByteArray name = pageRowMethod + "(QVariant,QVariant)" ; |
33 | if (auto index = metaObject->indexOfMethod(method: name.data()); index != -1) { |
34 | return metaObject->method(index).invoke(obj: object, arguments: page, arguments: QVariant::fromValue(value: properties)); |
35 | } else if (QQmlComponent *component = page.value<QQmlComponent *>(); component != nullptr) { |
36 | return metaObject->invokeMethod(obj: object, member: stackViewMethod.data(), arguments&: component, arguments: properties); |
37 | } else if (QQuickItem *item = page.value<QQuickItem *>(); item != nullptr) { |
38 | return metaObject->invokeMethod(obj: object, member: stackViewMethod.data(), arguments&: item, arguments: properties); |
39 | } else if (QUrl url = page.toUrl(); !url.isEmpty()) { |
40 | return metaObject->invokeMethod(obj: object, member: stackViewMethod.data(), arguments&: url, arguments: properties); |
41 | } |
42 | |
43 | return false; |
44 | } |
45 | |
46 | PageStackAttached::PageStackAttached(QObject *parent) |
47 | : QQuickAttachedPropertyPropagator(parent) |
48 | { |
49 | m_parentItem = qobject_cast<QQuickItem *>(o: parent); |
50 | |
51 | if (!m_parentItem) { |
52 | qCDebug(KirigamiLayoutsLog) << "PageStack must be attached to an Item" << parent; |
53 | return; |
54 | } |
55 | |
56 | if (hasStackCapabilities(candidate: m_parentItem)) { |
57 | setPageStack(m_parentItem); |
58 | } else if (!m_pageStack) { |
59 | QQuickItem *candidate = m_parentItem->parentItem(); |
60 | while (candidate) { |
61 | if (hasStackCapabilities(candidate)) { |
62 | qmlAttachedPropertiesObject<PageStackAttached>(obj: candidate, create: true); |
63 | break; |
64 | } |
65 | candidate = candidate->parentItem(); |
66 | } |
67 | } |
68 | |
69 | initialize(); |
70 | } |
71 | |
72 | QQuickItem *PageStackAttached::pageStack() const |
73 | { |
74 | return m_pageStack; |
75 | } |
76 | |
77 | void PageStackAttached::setPageStack(QQuickItem *pageStack) |
78 | { |
79 | if (!pageStack || m_pageStack == pageStack || !hasStackCapabilities(candidate: pageStack)) { |
80 | return; |
81 | } |
82 | |
83 | m_customStack = true; |
84 | m_pageStack = pageStack; |
85 | |
86 | propagatePageStack(pageStack); |
87 | |
88 | Q_EMIT pageStackChanged(); |
89 | } |
90 | |
91 | void PageStackAttached::propagatePageStack(QQuickItem *pageStack) |
92 | { |
93 | if (!pageStack) { |
94 | return; |
95 | } |
96 | |
97 | if (!m_customStack && m_pageStack != pageStack) { |
98 | m_pageStack = pageStack; |
99 | Q_EMIT pageStackChanged(); |
100 | } |
101 | |
102 | const auto stacks = attachedChildren(); |
103 | for (QQuickAttachedPropertyPropagator *child : stacks) { |
104 | PageStackAttached *stackAttached = qobject_cast<PageStackAttached *>(object: child); |
105 | if (stackAttached) { |
106 | stackAttached->propagatePageStack(pageStack: m_pageStack); |
107 | } |
108 | } |
109 | } |
110 | |
111 | void PageStackAttached::push(const QVariant &page, const QVariantMap &properties) |
112 | { |
113 | if (!m_pageStack) { |
114 | qCWarning(KirigamiLayoutsLog) << "Pushing in an empty PageStackAttached" ; |
115 | return; |
116 | } |
117 | |
118 | if (!tryCall(object: m_pageStack, pageRowMethod: "push" , stackViewMethod: "pushItem" , page, properties)) { |
119 | qCWarning(KirigamiLayoutsLog) << "Invalid parameters to push: " << page << properties; |
120 | } |
121 | } |
122 | |
123 | void PageStackAttached::replace(const QVariant &page, const QVariantMap &properties) |
124 | { |
125 | if (!m_pageStack) { |
126 | qCWarning(KirigamiLayoutsLog) << "replacing in an empty PageStackAttached" ; |
127 | return; |
128 | } |
129 | |
130 | if (!tryCall(object: m_pageStack, pageRowMethod: "replace" , stackViewMethod: "replaceCurrentItem" , page, properties)) { |
131 | qCWarning(KirigamiLayoutsLog) << "Invalid parameters to replace: " << page << properties; |
132 | } |
133 | } |
134 | |
135 | void PageStackAttached::pop(const QVariant &page) |
136 | { |
137 | if (!m_pageStack) { |
138 | qCWarning(KirigamiLayoutsLog) << "Pushing in an empty PageStackAttached" ; |
139 | return; |
140 | } |
141 | |
142 | if (callIfValid(object: m_pageStack, method: "pop(QVariant)" , args: page)) { |
143 | return; |
144 | } else if (page.canConvert<QQuickItem *>() && callIfValid(object: m_pageStack, method: "popToItem(QQuickItem*)" , args: page.value<QQuickItem *>())) { |
145 | return; |
146 | } else if (callIfValid(object: m_pageStack, method: "popCurrentItem()" )) { |
147 | return; |
148 | } |
149 | |
150 | qCWarning(KirigamiLayoutsLog) << "Pop operation failed on stack" << m_pageStack << "with page" << page; |
151 | } |
152 | |
153 | void PageStackAttached::clear() |
154 | { |
155 | if (!m_pageStack) { |
156 | qCWarning(KirigamiLayoutsLog) << "Clearing in an empty PageStackAttached" ; |
157 | return; |
158 | } |
159 | |
160 | if (!callIfValid(object: m_pageStack, method: "clear()" )) { |
161 | qCWarning(KirigamiLayoutsLog) << "Call to clear() failed" ; |
162 | } |
163 | } |
164 | |
165 | bool PageStackAttached::hasStackCapabilities(QQuickItem *candidate) |
166 | { |
167 | // Duck type the candidate in order to see if it can be used as a stack having the expected methods |
168 | auto metaObject = candidate->metaObject(); |
169 | Q_ASSERT(metaObject); |
170 | |
171 | auto = [metaObject](QByteArrayView , QByteArrayView stackViewMethod) -> bool { |
172 | // For PageRow, we require a single method that takes QVariant,QVariant as argument. |
173 | QByteArray name = pageRowMethod + "(QVariant,QVariant)" ; |
174 | if (metaObject->indexOfMethod(method: name.data()) != -1) { |
175 | return true; |
176 | } |
177 | |
178 | // For StackView, we require three variants of the method. |
179 | name = stackViewMethod + "(QQmlComponent*,QVariantMap)" ; |
180 | if (metaObject->indexOfMethod(method: name.data()) == -1) { |
181 | return false; |
182 | } |
183 | |
184 | name = stackViewMethod + "(QQuickItem*,QVariantMap)" ; |
185 | if (metaObject->indexOfMethod(method: name.data()) == -1) { |
186 | return false; |
187 | } |
188 | |
189 | name = stackViewMethod + "(QUrl,QVariantMap)" ; |
190 | if (metaObject->indexOfMethod(method: name.data()) == -1) { |
191 | return false; |
192 | } |
193 | |
194 | return true; |
195 | }; |
196 | |
197 | if (!hasPageRowOrStackViewMethod("push" , "pushItem" )) { |
198 | return false; |
199 | } |
200 | |
201 | if (!hasPageRowOrStackViewMethod("replace" , "replaceCurrentItem" )) { |
202 | return false; |
203 | } |
204 | |
205 | auto index = metaObject->indexOfMethod(method: "pop(QVariant)" ); |
206 | if (index == -1) { |
207 | index = metaObject->indexOfMethod(method: "popToItem(QQuickItem*)" ); |
208 | if (index == -1) { |
209 | return false; |
210 | } |
211 | index = metaObject->indexOfMethod(method: "popCurrentItem()" ); |
212 | if (index == -1) { |
213 | return false; |
214 | } |
215 | } |
216 | |
217 | index = metaObject->indexOfMethod(method: "clear()" ); |
218 | if (index == -1) { |
219 | return false; |
220 | } |
221 | |
222 | return true; |
223 | } |
224 | |
225 | PageStackAttached *PageStackAttached::qmlAttachedProperties(QObject *object) |
226 | { |
227 | return new PageStackAttached(object); |
228 | } |
229 | |
230 | void PageStackAttached::attachedParentChange(QQuickAttachedPropertyPropagator *newParent, QQuickAttachedPropertyPropagator *oldParent) |
231 | { |
232 | Q_UNUSED(oldParent); |
233 | |
234 | PageStackAttached *stackAttached = qobject_cast<PageStackAttached *>(object: newParent); |
235 | if (stackAttached) { |
236 | propagatePageStack(pageStack: stackAttached->pageStack()); |
237 | } |
238 | } |
239 | |
240 | #include "moc_pagestackattached.cpp" |
241 | |