Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qsemaphore.cpp
Go to the documentation of this file.
1// Copyright (C) 2022 The Qt Company Ltd.
2// Copyright (C) 2018 Intel Corporation.
3// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
4// Qt-Security score:significant reason:default
5
6#include "qsemaphore.h"
7#include "qfutex_p.h"
9#include "qdatetime.h"
10#include "qdebug.h"
11#include "qlocking_p.h"
13
14#include <chrono>
15#if !QT_CONFIG(thread)
16#include <limits>
17#endif
18
20
21using namespace QtFutex;
22
23#if QT_CONFIG(thread)
24
25/*!
26 \class QSemaphore
27 \inmodule QtCore
28 \brief The QSemaphore class provides a general counting semaphore.
29
30 \threadsafe
31
32 \ingroup thread
33
34 A semaphore is a generalization of a mutex. While a mutex can
35 only be locked once, it's possible to acquire a semaphore
36 multiple times. Semaphores are typically used to protect a
37 certain number of identical resources.
38
39 Semaphores support two fundamental operations, acquire() and
40 release():
41
42 \list
43 \li acquire(\e{n}) tries to acquire \e n resources. If there aren't
44 that many resources available, the call will block until this
45 is the case.
46 \li release(\e{n}) releases \e n resources.
47 \endlist
48
49 There's also a tryAcquire() function that returns immediately if
50 it cannot acquire the resources, and an available() function that
51 returns the number of available resources at any time.
52
53 Example:
54
55 \snippet code/src_corelib_thread_qsemaphore.cpp 0
56
57 A typical application of semaphores is for controlling access to
58 a circular buffer shared by a producer thread and a consumer
59 thread. The \l{Producer and Consumer using Semaphores} example shows how
60 to use QSemaphore to solve that problem.
61
62 A non-computing example of a semaphore would be dining at a
63 restaurant. A semaphore is initialized with the number of chairs
64 in the restaurant. As people arrive, they want a seat. As seats
65 are filled, available() is decremented. As people leave, the
66 available() is incremented, allowing more people to enter. If a
67 party of 10 people want to be seated, but there are only 9 seats,
68 those 10 people will wait, but a party of 4 people would be
69 seated (taking the available seats to 5, making the party of 10
70 people wait longer).
71
72 \sa QSemaphoreReleaser, QMutex, QWaitCondition, QThread,
73 {Producer and Consumer using Semaphores}
74*/
75
76/*
77 QSemaphore futex operation
78
79 QSemaphore stores a 32-bit integer with the counter of currently available
80 tokens (value between 0 and INT_MAX). When a thread attempts to acquire n
81 tokens and the counter is larger than that, we perform a compare-and-swap
82 with the new count. If that succeeds, the acquisition worked; if not, we
83 loop again because the counter changed. If there were not enough tokens,
84 we'll perform a futex-wait.
85
86 Before we do, we set the high bit in the futex to indicate that semaphore
87 is contended: that is, there's a thread waiting for more tokens. On
88 release() for n tokens, we perform a fetch-and-add of n and then check if
89 that high bit was set. If it was, then we clear that bit and perform a
90 futex-wake on the semaphore to indicate the waiting threads can wake up and
91 acquire tokens. Which ones get woken up is unspecified.
92
93 If the system has the ability to wake up a precise number of threads, has
94 Linux's FUTEX_WAKE_OP functionality, and is 64-bit, instead of using a
95 single bit indicating a contended semaphore, we'll store the number of
96 tokens *plus* total number of waiters in the high word. Additionally, all
97 multi-token waiters will be waiting on that high word. So when releasing n
98 tokens on those systems, we tell the kernel to wake up n single-token
99 threads and all of the multi-token ones. Which threads get woken up is
100 unspecified, but it's likely single-token threads will get woken up first.
101 */
102
103#if defined(FUTEX_OP) && QT_POINTER_SIZE > 4
104static constexpr bool futexHasWaiterCount = true;
105#else
106static constexpr bool futexHasWaiterCount = false;
107#endif
108
109static constexpr quintptr futexNeedsWakeAllBit = futexHasWaiterCount ?
110 (Q_UINT64_C(1) << (sizeof(quintptr) * CHAR_BIT - 1)) : 0x80000000U;
111
112static int futexAvailCounter(quintptr v)
113{
114 // the low 31 bits
115 if (futexHasWaiterCount) {
116 // the high bit of the low word isn't used
117 Q_ASSERT((v & 0x80000000U) == 0);
118
119 // so we can be a little faster
120 return int(unsigned(v));
121 }
122 return int(v & 0x7fffffffU);
123}
124
125static bool futexNeedsWake(quintptr v)
126{
127 // If we're counting waiters, the number of waiters plus value is stored in the
128 // low 31 bits of the high word (that is, bits 32-62). If we're not, then we only
129 // use futexNeedsWakeAllBit to indicate anyone is waiting.
130 if constexpr (futexHasWaiterCount)
131 return unsigned(quint64(v) >> 32) > unsigned(v);
132 return v >> 31;
133}
134
135static QBasicAtomicInteger<quint32> *futexLow32(QBasicAtomicInteger<quintptr> *ptr)
136{
137 auto result = reinterpret_cast<QBasicAtomicInteger<quint32> *>(ptr);
138#if Q_BYTE_ORDER == Q_BIG_ENDIAN && QT_POINTER_SIZE > 4
139 ++result;
140#endif
141 return result;
142}
143
144static QBasicAtomicInteger<quint32> *futexHigh32(QBasicAtomicInteger<quintptr> *ptr)
145{
146 Q_ASSERT(futexHasWaiterCount);
147 auto result = reinterpret_cast<QBasicAtomicInteger<quint32> *>(ptr);
148#if Q_BYTE_ORDER == Q_LITTLE_ENDIAN && QT_POINTER_SIZE > 4
149 ++result;
150#endif
151 return result;
152}
153
154template <bool IsTimed> bool
155futexSemaphoreTryAcquire_loop(QBasicAtomicInteger<quintptr> &u, quintptr curValue, quintptr nn,
156 QDeadlineTimer timer)
157{
158 using namespace std::chrono;
159 int n = int(unsigned(nn));
160
161 // we're called after one testAndSet, so start by waiting first
162 for (;;) {
163 // indicate we're waiting
164 auto ptr = futexLow32(&u);
165 if (n > 1 || !futexHasWaiterCount) {
166 u.fetchAndOrRelaxed(futexNeedsWakeAllBit);
167 curValue |= futexNeedsWakeAllBit;
168 if constexpr (futexHasWaiterCount) {
169 Q_ASSERT(n > 1);
170 ptr = futexHigh32(&u);
171 curValue = quint64(curValue) >> 32;
172 }
173 }
174
175 if (IsTimed) {
176 bool timedout = !futexWait(*ptr, curValue, timer);
177 if (timedout)
178 return false;
179 } else {
180 futexWait(*ptr, curValue);
181 }
182
183 curValue = u.loadAcquire();
184
185 // try to acquire
186 while (futexAvailCounter(curValue) >= n) {
187 quintptr newValue = curValue - nn;
188 if (u.testAndSetOrdered(curValue, newValue, curValue))
189 return true; // succeeded!
190 }
191
192 // not enough tokens available, put us to wait
193 if (IsTimed && timer.hasExpired())
194 return false;
195 }
196}
197
198static constexpr QDeadlineTimer::ForeverConstant Expired =
199 QDeadlineTimer::ForeverConstant(1);
200
201template <typename T> bool
202futexSemaphoreTryAcquire(QBasicAtomicInteger<quintptr> &u, int n, T timeout)
203{
204 constexpr bool IsTimed = std::is_same_v<QDeadlineTimer, T>;
205 // Try to acquire without waiting (we still loop because the testAndSet
206 // call can fail).
207 quintptr nn = unsigned(n);
208 if (futexHasWaiterCount)
209 nn |= quint64(nn) << 32; // token count replicated in high word
210
211 quintptr curValue = u.loadAcquire();
212 while (futexAvailCounter(curValue) >= n) {
213 // try to acquire
214 quintptr newValue = curValue - nn;
215 if (u.testAndSetOrdered(curValue, newValue, curValue))
216 return true; // succeeded!
217 }
218 if constexpr (IsTimed) {
219 if (timeout.hasExpired())
220 return false;
221 } else {
222 if (timeout == Expired)
223 return false;
224 }
225
226 // we need to wait
227 constexpr quintptr oneWaiter = quintptr(Q_UINT64_C(1) << 32); // zero on 32-bit
228 if constexpr (futexHasWaiterCount) {
229 // We don't use the fetched value from above so futexWait() fails if
230 // it changed after the testAndSetOrdered above.
231 quint32 waiterCount = (quint64(curValue) >> 32) & 0x7fffffffU;
232 if (waiterCount == 0x7fffffffU) {
233 qCritical() << "Waiter count overflow in QSemaphore";
234 return false;
235 }
236
237 // increase the waiter count
238 u.fetchAndAddRelaxed(oneWaiter);
239 curValue += oneWaiter;
240
241 // Also adjust nn to subtract oneWaiter when we succeed in acquiring.
242 nn += oneWaiter;
243 }
244
245 if (futexSemaphoreTryAcquire_loop<IsTimed>(u, curValue, nn, timeout))
246 return true;
247
248 Q_ASSERT(IsTimed);
249
250 if (futexHasWaiterCount) {
251 // decrement the number of threads waiting
252 Q_ASSERT(futexHigh32(&u)->loadRelaxed() & 0x7fffffffU);
253 u.fetchAndSubRelaxed(oneWaiter);
254 }
255 return false;
256}
257
258namespace QtSemaphorePrivate {
259using namespace QtPrivate;
260struct Layout1
261{
262 alignas(IdealMutexAlignment) std::mutex mutex;
263 qsizetype avail = 0;
264 alignas(IdealMutexAlignment) std::condition_variable cond;
265};
266
267struct Layout2
268{
269 alignas(IdealMutexAlignment) std::mutex mutex;
270 alignas(IdealMutexAlignment) std::condition_variable cond;
271 qsizetype avail = 0;
272};
273
274// Choose Layout1 if it is smaller than Layout2. That happens for platforms
275// where sizeof(mutex) is 64.
276using Members = std::conditional_t<sizeof(Layout1) <= sizeof(Layout2), Layout1, Layout2>;
277} // namespace QtSemaphorePrivate
278
279class QSemaphorePrivate : public QtSemaphorePrivate::Members
280{
281public:
282 explicit QSemaphorePrivate(qsizetype n) { avail = n; }
283};
284
285/*!
286 Creates a new semaphore and initializes the number of resources
287 it guards to \a n (by default, 0).
288
289 \sa release(), available()
290*/
291QSemaphore::QSemaphore(int n)
292{
293 Q_ASSERT_X(n >= 0, "QSemaphore", "parameter 'n' must be non-negative");
294 if (futexAvailable()) {
295 quintptr nn = unsigned(n);
296 if (futexHasWaiterCount)
297 nn |= quint64(nn) << 32; // token count replicated in high word
298 u.storeRelaxed(nn);
299 } else {
300 d = new QSemaphorePrivate(n);
301 }
302}
303
304/*!
305 Destroys the semaphore.
306
307 \warning Destroying a semaphore that is in use may result in
308 undefined behavior.
309*/
310QSemaphore::~QSemaphore()
311{
312 if (!futexAvailable())
313 delete d;
314}
315
316/*!
317 Tries to acquire \c n resources guarded by the semaphore. If \a n
318 > available(), this call will block until enough resources are
319 available.
320
321 \sa release(), available(), tryAcquire()
322*/
323void QSemaphore::acquire(int n)
324{
325#if QT_VERSION >= QT_VERSION_CHECK(7, 0, 0)
326# warning "Move the Q_ASSERT to inline code, make QSemaphore have wide contract, "
327 "and mark noexcept where futexes are in use."
328#else
329 Q_ASSERT_X(n >= 0, "QSemaphore::acquire", "parameter 'n' must be non-negative");
330#endif
331
332 if (futexAvailable()) {
333 futexSemaphoreTryAcquire(u, n, QDeadlineTimer::Forever);
334 return;
335 }
336
337 const auto sufficientResourcesAvailable = [this, n] { return d->avail >= n; };
338
339 auto locker = qt_unique_lock(d->mutex);
340 d->cond.wait(locker, sufficientResourcesAvailable);
341 d->avail -= n;
342}
343
344/*!
345 Releases \a n resources guarded by the semaphore.
346
347 This function can be used to "create" resources as well. For
348 example:
349
350 \snippet code/src_corelib_thread_qsemaphore.cpp 1
351
352 QSemaphoreReleaser is a \l{http://en.cppreference.com/w/cpp/language/raii}{RAII}
353 wrapper around this function.
354
355 \sa acquire(), available(), QSemaphoreReleaser
356*/
357void QSemaphore::release(int n)
358{
359 Q_ASSERT_X(n >= 0, "QSemaphore::release", "parameter 'n' must be non-negative");
360
361 if (futexAvailable()) {
362 quintptr nn = unsigned(n);
363 if (futexHasWaiterCount)
364 nn |= quint64(nn) << 32; // token count replicated in high word
365 quintptr prevValue = u.loadRelaxed();
366 quintptr newValue;
367 do { // loop just to ensure the operations are done atomically
368 newValue = prevValue + nn;
369 newValue &= (futexNeedsWakeAllBit - 1);
370 } while (!u.testAndSetRelease(prevValue, newValue, prevValue));
371 if (futexNeedsWake(prevValue)) {
372#ifdef FUTEX_OP
373 if (futexHasWaiterCount) {
374 /*
375 On 64-bit systems, the single-token waiters wait on the low half
376 and the multi-token waiters wait on the upper half. So we ask
377 the kernel to wake up n single-token waiters and all multi-token
378 waiters (if any), and clear the multi-token wait bit.
379
380 atomic {
381 int oldval = *upper;
382 *upper = oldval | 0;
383 futexWake(lower, n);
384 if (oldval != 0) // always true
385 futexWake(upper, INT_MAX);
386 }
387 */
388 quint32 op = FUTEX_OP_OR;
389 quint32 oparg = 0;
390 quint32 cmp = FUTEX_OP_CMP_NE;
391 quint32 cmparg = 0;
392 futexWakeOp(*futexLow32(&u), n, INT_MAX, *futexHigh32(&u), FUTEX_OP(op, oparg, cmp, cmparg));
393 return;
394 }
395#endif
396 // Unset the bit and wake everyone. There are two possibilities
397 // under which a thread can set the bit between the AND and the
398 // futexWake:
399 // 1) it did see the new counter value, but it wasn't enough for
400 // its acquisition anyway, so it has to wait;
401 // 2) it did not see the new counter value, in which case its
402 // futexWait will fail.
403 futexWakeAll(*futexLow32(&u));
404 if (futexHasWaiterCount)
405 futexWakeAll(*futexHigh32(&u));
406 }
407 return;
408 }
409
410 // Keep mutex locked until after notify_all() lest another thread acquire()s
411 // the semaphore once d->avail == 0 and then destroys it, leaving `d` dangling.
412 const auto locker = qt_scoped_lock(d->mutex);
413 d->avail += n;
414 d->cond.notify_all();
415}
416
417/*!
418 Returns the number of resources currently available to the
419 semaphore. This number can never be negative.
420
421 \sa acquire(), release()
422*/
423int QSemaphore::available() const
424{
425 if (futexAvailable())
426 return futexAvailCounter(u.loadRelaxed());
427
428 const auto locker = qt_scoped_lock(d->mutex);
429 return d->avail;
430}
431
432/*!
433 Tries to acquire \c n resources guarded by the semaphore and
434 returns \c true on success. If available() < \a n, this call
435 immediately returns \c false without acquiring any resources.
436
437 Example:
438
439 \snippet code/src_corelib_thread_qsemaphore.cpp 2
440
441 \sa acquire()
442*/
443bool QSemaphore::tryAcquire(int n)
444{
445 Q_ASSERT_X(n >= 0, "QSemaphore::tryAcquire", "parameter 'n' must be non-negative");
446
447 if (futexAvailable())
448 return futexSemaphoreTryAcquire(u, n, Expired);
449
450 const auto locker = qt_scoped_lock(d->mutex);
451 if (n > d->avail)
452 return false;
453 d->avail -= n;
454 return true;
455}
456
457/*!
458 \fn QSemaphore::tryAcquire(int n, int timeout)
459
460 Tries to acquire \c n resources guarded by the semaphore and
461 returns \c true on success. If available() < \a n, this call will
462 wait for at most \a timeout milliseconds for resources to become
463 available.
464
465 Note: Passing a negative number as the \a timeout is equivalent to
466 calling acquire(), i.e. this function will wait forever for
467 resources to become available if \a timeout is negative.
468
469 Example:
470
471 \snippet code/src_corelib_thread_qsemaphore.cpp 3
472
473 \sa acquire()
474*/
475
476/*!
477 \since 6.6
478
479 Tries to acquire \c n resources guarded by the semaphore and returns \c
480 true on success. If available() < \a n, this call will wait until \a timer
481 expires for resources to become available.
482
483 Example:
484
485 \snippet code/src_corelib_thread_qsemaphore.cpp tryAcquire-QDeadlineTimer
486
487 \sa acquire()
488*/
489bool QSemaphore::tryAcquire(int n, QDeadlineTimer timer)
490{
491 if (timer.isForever()) {
492 acquire(n);
493 return true;
494 }
495
496 if (timer.hasExpired())
497 return tryAcquire(n);
498
499 Q_ASSERT_X(n >= 0, "QSemaphore::tryAcquire", "parameter 'n' must be non-negative");
500
501 if (futexAvailable())
502 return futexSemaphoreTryAcquire(u, n, timer);
503
504 using namespace std::chrono;
505 const auto sufficientResourcesAvailable = [this, n] { return d->avail >= n; };
506
507 auto locker = qt_unique_lock(d->mutex);
508 if (!d->cond.wait_until(locker, timer.deadline<steady_clock>(), sufficientResourcesAvailable))
509 return false;
510 d->avail -= n;
511 return true;
512}
513
514/*!
515 \fn template <typename Rep, typename Period> QSemaphore::tryAcquire(int n, std::chrono::duration<Rep, Period> timeout)
516 \overload
517 \since 6.3
518*/
519
520/*!
521 \fn bool QSemaphore::try_acquire()
522 \since 6.3
523
524 This function is provided for \c{std::counting_semaphore} compatibility.
525
526 It is equivalent to calling \c{tryAcquire(1)}, where the function returns
527 \c true on acquiring the resource successfully.
528
529 \sa tryAcquire(), try_acquire_for(), try_acquire_until()
530*/
531
532/*!
533 \fn template <typename Rep, typename Period> bool QSemaphore::try_acquire_for(const std::chrono::duration<Rep, Period> &timeout)
534 \since 6.3
535
536 This function is provided for \c{std::counting_semaphore} compatibility.
537
538 It is equivalent to calling \c{tryAcquire(1, timeout)}, where the call
539 times out on the given \a timeout value. The function returns \c true
540 on acquiring the resource successfully.
541
542 \sa tryAcquire(), try_acquire(), try_acquire_until()
543*/
544
545/*!
546 \fn template <typename Clock, typename Duration> bool QSemaphore::try_acquire_until(const std::chrono::time_point<Clock, Duration> &tp)
547 \since 6.3
548
549 This function is provided for \c{std::counting_semaphore} compatibility.
550
551 It is equivalent to calling \c{tryAcquire(1, tp - Clock::now())},
552 which means that the \a tp (time point) is recorded, ignoring the
553 adjustments to \c{Clock} while waiting. The function returns \c true
554 on acquiring the resource successfully.
555
556 \sa tryAcquire(), try_acquire(), try_acquire_for()
557*/
558
559/*!
560 \class QSemaphoreReleaser
561 \brief The QSemaphoreReleaser class provides exception-safe deferral of a QSemaphore::release() call.
562 \since 5.10
563 \ingroup thread
564 \inmodule QtCore
565
566 \reentrant
567
568 QSemaphoreReleaser can be used wherever you would otherwise use
569 QSemaphore::release(). Constructing a QSemaphoreReleaser defers the
570 release() call on the semaphore until the QSemaphoreReleaser is
571 destroyed (see
572 \l{http://en.cppreference.com/w/cpp/language/raii}{RAII pattern}).
573
574 You can use this to reliably release a semaphore to avoid dead-lock
575 in the face of exceptions or early returns:
576
577 \snippet code/src_corelib_thread_qsemaphore.cpp 4
578
579 If an early return is taken or an exception is thrown before the
580 \c{sem.release()} call is reached, the semaphore is not released,
581 possibly preventing the thread waiting in the corresponding
582 \c{sem.acquire()} call from ever continuing execution.
583
584 When using RAII instead:
585
586 \snippet code/src_corelib_thread_qsemaphore.cpp 5
587
588 this can no longer happen, because the compiler will make sure that
589 the QSemaphoreReleaser destructor is always called, and therefore
590 the semaphore is always released.
591
592 QSemaphoreReleaser is move-enabled and can therefore be returned
593 from functions to transfer responsibility for releasing a semaphore
594 out of a function or a scope:
595
596 \snippet code/src_corelib_thread_qsemaphore.cpp 6
597
598 A QSemaphoreReleaser can be canceled by a call to cancel(). A canceled
599 semaphore releaser will no longer call QSemaphore::release() in its
600 destructor.
601
602 \sa QMutexLocker
603*/
604
605/*!
606 \fn QSemaphoreReleaser::QSemaphoreReleaser()
607
608 Default constructor. Creates a QSemaphoreReleaser that does nothing.
609*/
610
611/*!
612 \fn QSemaphoreReleaser::QSemaphoreReleaser(QSemaphore &sem, int n)
613
614 Constructor. Stores the arguments and calls \a{sem}.release(\a{n})
615 in the destructor.
616*/
617
618/*!
619 \fn QSemaphoreReleaser::QSemaphoreReleaser(QSemaphore *sem, int n)
620
621 Constructor. Stores the arguments and calls \a{sem}->release(\a{n})
622 in the destructor.
623*/
624
625/*!
626 \fn QSemaphoreReleaser::QSemaphoreReleaser(QSemaphoreReleaser &&other)
627
628 Move constructor. Takes over responsibility to call QSemaphore::release()
629 from \a other, which in turn is canceled.
630
631 \sa cancel()
632*/
633
634/*!
635 \fn QSemaphoreReleaser::operator=(QSemaphoreReleaser &&other)
636
637 Move assignment operator. Takes over responsibility to call QSemaphore::release()
638 from \a other, which in turn is canceled.
639
640 If this semaphore releaser had the responsibility to call some QSemaphore::release()
641 itself, it performs the call before taking over from \a other.
642
643 \sa cancel()
644*/
645
646/*!
647 \fn QSemaphoreReleaser::~QSemaphoreReleaser()
648
649 Unless canceled, calls QSemaphore::release() with the arguments provided
650 to the constructor, or by the last move assignment.
651*/
652
653/*!
654 \fn QSemaphoreReleaser::swap(QSemaphoreReleaser &other)
655
656 Exchanges the responsibilities of \c{*this} and \a other.
657
658 Unlike move assignment, neither of the two objects ever releases its
659 semaphore, if any, as a consequence of swapping.
660
661 Therefore this function is very fast and never fails.
662*/
663
664/*!
665 \fn QSemaphoreReleaser::semaphore() const
666
667 Returns a pointer to the QSemaphore object provided to the constructor,
668 or by the last move assignment, if any. Otherwise, returns \nullptr.
669*/
670
671/*!
672 \fn QSemaphoreReleaser::cancel()
673
674 Cancels this QSemaphoreReleaser such that the destructor will no longer
675 call \c{semaphore()->release()}. Returns the value of semaphore()
676 before this call. After this call, semaphore() will return \nullptr.
677
678 To enable again, assign a new QSemaphoreReleaser:
679
680 \snippet code/src_corelib_thread_qsemaphore.cpp 7
681*/
682
683#else // #if QT_CONFIG(thread)
684
685// No-thread stubs for QSemaphore. These essentially allow
686// unlimited acquire and release, since we can't ever block
687// the calling thread (which is the only thread in the no-thread
688// configuraton)
689
690QSemaphore::QSemaphore(int)
691{
692
693}
694
695QSemaphore::~QSemaphore()
696{
697
698}
699
700void QSemaphore::acquire(int)
701{
702
703}
704
705void QSemaphore::release(int)
706{
707
708}
709
710int QSemaphore::available() const
711{
712 return std::numeric_limits<int>::max();
713}
714
715bool QSemaphore::tryAcquire(int)
716{
717 return true;
718}
719
720bool QSemaphore::tryAcquire(int, QDeadlineTimer)
721{
722 return true;
723}
724
725#endif
726
727QT_END_NAMESPACE
Combined button and popup list for selecting options.