diff options
author | Lars Knoll <lars.knoll@nokia.com> | 2011-05-07 00:02:01 +0200 |
---|---|---|
committer | Lars Knoll <lars.knoll@nokia.com> | 2011-05-07 00:02:01 +0200 |
commit | f67b8df3ebdba2d398b9cce686b7c644adffff08 (patch) | |
tree | 062dd469f7cf8daa01a32d3e7b767b8fbdb7573a /src/widgets/util | |
parent | 32ce4fe9e6a94e77828e976776cf08da85254ff2 (diff) | |
download | qtbase-f67b8df3ebdba2d398b9cce686b7c644adffff08.tar.gz |
library split
Diffstat (limited to 'src/widgets/util')
28 files changed, 11753 insertions, 0 deletions
diff --git a/src/widgets/util/qcompleter.cpp b/src/widgets/util/qcompleter.cpp new file mode 100644 index 0000000000..0cb3bbdf8c --- /dev/null +++ b/src/widgets/util/qcompleter.cpp @@ -0,0 +1,1833 @@ +/**************************************************************************** +** +** Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies). +** All rights reserved. +** Contact: Nokia Corporation (qt-info@nokia.com) +** +** This file is part of the QtGui module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** No Commercial Usage +** This file contains pre-release code and may not be distributed. +** You may use this file in accordance with the terms and conditions +** contained in the Technology Preview License Agreement accompanying +** this package. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain additional +** rights. These rights are described in the Nokia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** If you have questions regarding the use of this file, please contact +** Nokia at qt-info@nokia.com. +** +** +** +** +** +** +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +/*! + \class QCompleter + \brief The QCompleter class provides completions based on an item model. + \since 4.2 + + You can use QCompleter to provide auto completions in any Qt + widget, such as QLineEdit and QComboBox. + When the user starts typing a word, QCompleter suggests possible ways of + completing the word, based on a word list. The word list is + provided as a QAbstractItemModel. (For simple applications, where + the word list is static, you can pass a QStringList to + QCompleter's constructor.) + + \tableofcontents + + \section1 Basic Usage + + A QCompleter is used typically with a QLineEdit or QComboBox. + For example, here's how to provide auto completions from a simple + word list in a QLineEdit: + + \snippet doc/src/snippets/code/src_gui_util_qcompleter.cpp 0 + + A QFileSystemModel can be used to provide auto completion of file names. + For example: + + \snippet doc/src/snippets/code/src_gui_util_qcompleter.cpp 1 + + To set the model on which QCompleter should operate, call + setModel(). By default, QCompleter will attempt to match the \l + {completionPrefix}{completion prefix} (i.e., the word that the + user has started typing) against the Qt::EditRole data stored in + column 0 in the model case sensitively. This can be changed + using setCompletionRole(), setCompletionColumn(), and + setCaseSensitivity(). + + If the model is sorted on the column and role that are used for completion, + you can call setModelSorting() with either + QCompleter::CaseSensitivelySortedModel or + QCompleter::CaseInsensitivelySortedModel as the argument. On large models, + this can lead to significant performance improvements, because QCompleter + can then use binary search instead of linear search. + + The model can be a \l{QAbstractListModel}{list model}, + a \l{QAbstractTableModel}{table model}, or a + \l{QAbstractItemModel}{tree model}. Completion on tree models + is slightly more involved and is covered in the \l{Handling + Tree Models} section below. + + The completionMode() determines the mode used to provide completions to + the user. + + \section1 Iterating Through Completions + + To retrieve a single candidate string, call setCompletionPrefix() + with the text that needs to be completed and call + currentCompletion(). You can iterate through the list of + completions as below: + + \snippet doc/src/snippets/code/src_gui_util_qcompleter.cpp 2 + + completionCount() returns the total number of completions for the + current prefix. completionCount() should be avoided when possible, + since it requires a scan of the entire model. + + \section1 The Completion Model + + completionModel() return a list model that contains all possible + completions for the current completion prefix, in the order in which + they appear in the model. This model can be used to display the current + completions in a custom view. Calling setCompletionPrefix() automatically + refreshes the completion model. + + \section1 Handling Tree Models + + QCompleter can look for completions in tree models, assuming + that any item (or sub-item or sub-sub-item) can be unambiguously + represented as a string by specifying the path to the item. The + completion is then performed one level at a time. + + Let's take the example of a user typing in a file system path. + The model is a (hierarchical) QFileSystemModel. The completion + occurs for every element in the path. For example, if the current + text is \c C:\Wind, QCompleter might suggest \c Windows to + complete the current path element. Similarly, if the current text + is \c C:\Windows\Sy, QCompleter might suggest \c System. + + For this kind of completion to work, QCompleter needs to be able to + split the path into a list of strings that are matched at each level. + For \c C:\Windows\Sy, it needs to be split as "C:", "Windows" and "Sy". + The default implementation of splitPath(), splits the completionPrefix + using QDir::separator() if the model is a QFileSystemModel. + + To provide completions, QCompleter needs to know the path from an index. + This is provided by pathFromIndex(). The default implementation of + pathFromIndex(), returns the data for the \l{Qt::EditRole}{edit role} + for list models and the absolute file path if the mode is a QFileSystemModel. + + \sa QAbstractItemModel, QLineEdit, QComboBox, {Completer Example} +*/ + +#include "qcompleter_p.h" + +#ifndef QT_NO_COMPLETER + +#include "QtGui/qscrollbar.h" +#include "QtGui/qstringlistmodel.h" +#include "QtGui/qdirmodel.h" +#include "QtGui/qfilesystemmodel.h" +#include "QtGui/qheaderview.h" +#include "QtGui/qlistview.h" +#include "QtGui/qapplication.h" +#include "QtGui/qevent.h" +#include "QtGui/qheaderview.h" +#include "QtGui/qdesktopwidget.h" +#include "QtGui/qlineedit.h" + +QT_BEGIN_NAMESPACE + +QCompletionModel::QCompletionModel(QCompleterPrivate *c, QObject *parent) + : QAbstractProxyModel(*new QCompletionModelPrivate, parent), + c(c), showAll(false) +{ + createEngine(); +} + +int QCompletionModel::columnCount(const QModelIndex &) const +{ + Q_D(const QCompletionModel); + return d->model->columnCount(); +} + +void QCompletionModel::setSourceModel(QAbstractItemModel *source) +{ + bool hadModel = (sourceModel() != 0); + + if (hadModel) + QObject::disconnect(sourceModel(), 0, this, 0); + + QAbstractProxyModel::setSourceModel(source); + + if (source) { + // TODO: Optimize updates in the source model + connect(source, SIGNAL(modelReset()), this, SLOT(invalidate())); + connect(source, SIGNAL(destroyed()), this, SLOT(modelDestroyed())); + connect(source, SIGNAL(layoutChanged()), this, SLOT(invalidate())); + connect(source, SIGNAL(rowsInserted(QModelIndex,int,int)), this, SLOT(rowsInserted())); + connect(source, SIGNAL(rowsRemoved(QModelIndex,int,int)), this, SLOT(invalidate())); + connect(source, SIGNAL(columnsInserted(QModelIndex,int,int)), this, SLOT(invalidate())); + connect(source, SIGNAL(columnsRemoved(QModelIndex,int,int)), this, SLOT(invalidate())); + connect(source, SIGNAL(dataChanged(QModelIndex,QModelIndex)), this, SLOT(invalidate())); + } + + invalidate(); +} + +void QCompletionModel::createEngine() +{ + bool sortedEngine = false; + switch (c->sorting) { + case QCompleter::UnsortedModel: + sortedEngine = false; + break; + case QCompleter::CaseSensitivelySortedModel: + sortedEngine = c->cs == Qt::CaseSensitive; + break; + case QCompleter::CaseInsensitivelySortedModel: + sortedEngine = c->cs == Qt::CaseInsensitive; + break; + } + + if (sortedEngine) + engine.reset(new QSortedModelEngine(c)); + else + engine.reset(new QUnsortedModelEngine(c)); +} + +QModelIndex QCompletionModel::mapToSource(const QModelIndex& index) const +{ + Q_D(const QCompletionModel); + if (!index.isValid()) + return engine->curParent; + + int row; + QModelIndex parent = engine->curParent; + if (!showAll) { + if (!engine->matchCount()) + return QModelIndex(); + Q_ASSERT(index.row() < engine->matchCount()); + QIndexMapper& rootIndices = engine->historyMatch.indices; + if (index.row() < rootIndices.count()) { + row = rootIndices[index.row()]; + parent = QModelIndex(); + } else { + row = engine->curMatch.indices[index.row() - rootIndices.count()]; + } + } else { + row = index.row(); + } + + return d->model->index(row, index.column(), parent); +} + +QModelIndex QCompletionModel::mapFromSource(const QModelIndex& idx) const +{ + if (!idx.isValid()) + return QModelIndex(); + + int row = -1; + if (!showAll) { + if (!engine->matchCount()) + return QModelIndex(); + + QIndexMapper& rootIndices = engine->historyMatch.indices; + if (idx.parent().isValid()) { + if (idx.parent() != engine->curParent) + return QModelIndex(); + } else { + row = rootIndices.indexOf(idx.row()); + if (row == -1 && engine->curParent.isValid()) + return QModelIndex(); // source parent and our parent don't match + } + + if (row == -1) { + QIndexMapper& indices = engine->curMatch.indices; + engine->filterOnDemand(idx.row() - indices.last()); + row = indices.indexOf(idx.row()) + rootIndices.count(); + } + + if (row == -1) + return QModelIndex(); + } else { + if (idx.parent() != engine->curParent) + return QModelIndex(); + row = idx.row(); + } + + return createIndex(row, idx.column()); +} + +bool QCompletionModel::setCurrentRow(int row) +{ + if (row < 0 || !engine->matchCount()) + return false; + + if (row >= engine->matchCount()) + engine->filterOnDemand(row + 1 - engine->matchCount()); + + if (row >= engine->matchCount()) // invalid row + return false; + + engine->curRow = row; + return true; +} + +QModelIndex QCompletionModel::currentIndex(bool sourceIndex) const +{ + if (!engine->matchCount()) + return QModelIndex(); + + int row = engine->curRow; + if (showAll) + row = engine->curMatch.indices[engine->curRow]; + + QModelIndex idx = createIndex(row, c->column); + if (!sourceIndex) + return idx; + return mapToSource(idx); +} + +QModelIndex QCompletionModel::index(int row, int column, const QModelIndex& parent) const +{ + Q_D(const QCompletionModel); + if (row < 0 || column < 0 || column >= columnCount(parent) || parent.isValid()) + return QModelIndex(); + + if (!showAll) { + if (!engine->matchCount()) + return QModelIndex(); + if (row >= engine->historyMatch.indices.count()) { + int want = row + 1 - engine->matchCount(); + if (want > 0) + engine->filterOnDemand(want); + if (row >= engine->matchCount()) + return QModelIndex(); + } + } else { + if (row >= d->model->rowCount(engine->curParent)) + return QModelIndex(); + } + + return createIndex(row, column); +} + +int QCompletionModel::completionCount() const +{ + if (!engine->matchCount()) + return 0; + + engine->filterOnDemand(INT_MAX); + return engine->matchCount(); +} + +int QCompletionModel::rowCount(const QModelIndex &parent) const +{ + Q_D(const QCompletionModel); + if (parent.isValid()) + return 0; + + if (showAll) { + // Show all items below current parent, even if we have no valid matches + if (engine->curParts.count() != 1 && !engine->matchCount() + && !engine->curParent.isValid()) + return 0; + return d->model->rowCount(engine->curParent); + } + + return completionCount(); +} + +void QCompletionModel::setFiltered(bool filtered) +{ + if (showAll == !filtered) + return; + showAll = !filtered; + resetModel(); +} + +bool QCompletionModel::hasChildren(const QModelIndex &parent) const +{ + Q_D(const QCompletionModel); + if (parent.isValid()) + return false; + + if (showAll) + return d->model->hasChildren(mapToSource(parent)); + + if (!engine->matchCount()) + return false; + + return true; +} + +QVariant QCompletionModel::data(const QModelIndex& index, int role) const +{ + Q_D(const QCompletionModel); + return d->model->data(mapToSource(index), role); +} + +void QCompletionModel::modelDestroyed() +{ + QAbstractProxyModel::setSourceModel(0); // switch to static empty model + invalidate(); +} + +void QCompletionModel::rowsInserted() +{ + invalidate(); + emit rowsAdded(); +} + +void QCompletionModel::invalidate() +{ + engine->cache.clear(); + filter(engine->curParts); +} + +void QCompletionModel::filter(const QStringList& parts) +{ + Q_D(QCompletionModel); + engine->filter(parts); + resetModel(); + + if (d->model->canFetchMore(engine->curParent)) + d->model->fetchMore(engine->curParent); +} + +void QCompletionModel::resetModel() +{ + if (rowCount() == 0) { + reset(); + return; + } + + emit layoutAboutToBeChanged(); + QModelIndexList piList = persistentIndexList(); + QModelIndexList empty; + for (int i = 0; i < piList.size(); i++) + empty.append(QModelIndex()); + changePersistentIndexList(piList, empty); + emit layoutChanged(); +} + +////////////////////////////////////////////////////////////////////////////// +void QCompletionEngine::filter(const QStringList& parts) +{ + const QAbstractItemModel *model = c->proxy->sourceModel(); + curParts = parts; + if (curParts.isEmpty()) + curParts.append(QString()); + + curRow = -1; + curParent = QModelIndex(); + curMatch = QMatchData(); + historyMatch = filterHistory(); + + if (!model) + return; + + QModelIndex parent; + for (int i = 0; i < curParts.count() - 1; i++) { + QString part = curParts[i]; + int emi = filter(part, parent, -1).exactMatchIndex; + if (emi == -1) + return; + parent = model->index(emi, c->column, parent); + } + + // Note that we set the curParent to a valid parent, even if we have no matches + // When filtering is disabled, we show all the items under this parent + curParent = parent; + if (curParts.last().isEmpty()) + curMatch = QMatchData(QIndexMapper(0, model->rowCount(curParent) - 1), -1, false); + else + curMatch = filter(curParts.last(), curParent, 1); // build at least one + curRow = curMatch.isValid() ? 0 : -1; +} + +QMatchData QCompletionEngine::filterHistory() +{ + QAbstractItemModel *source = c->proxy->sourceModel(); + if (curParts.count() <= 1 || c->proxy->showAll || !source) + return QMatchData(); + bool isDirModel = false; + bool isFsModel = false; +#ifndef QT_NO_DIRMODEL + isDirModel = (qobject_cast<QDirModel *>(source) != 0); +#endif +#ifndef QT_NO_FILESYSTEMMODEL + isFsModel = (qobject_cast<QFileSystemModel *>(source) != 0); +#endif + QVector<int> v; + QIndexMapper im(v); + QMatchData m(im, -1, true); + + for (int i = 0; i < source->rowCount(); i++) { + QString str = source->index(i, c->column).data().toString(); + if (str.startsWith(c->prefix, c->cs) +#if (!defined(Q_OS_WIN) || defined(Q_OS_WINCE)) && !defined(Q_OS_SYMBIAN) + && ((!isFsModel && !isDirModel) || QDir::toNativeSeparators(str) != QDir::separator()) +#endif + ) + m.indices.append(i); + } + return m; +} + +// Returns a match hint from the cache by chopping the search string +bool QCompletionEngine::matchHint(QString part, const QModelIndex& parent, QMatchData *hint) +{ + if (c->cs == Qt::CaseInsensitive) + part = part.toLower(); + + const CacheItem& map = cache[parent]; + + QString key = part; + while (!key.isEmpty()) { + key.chop(1); + if (map.contains(key)) { + *hint = map[key]; + return true; + } + } + + return false; +} + +bool QCompletionEngine::lookupCache(QString part, const QModelIndex& parent, QMatchData *m) +{ + if (c->cs == Qt::CaseInsensitive) + part = part.toLower(); + const CacheItem& map = cache[parent]; + if (!map.contains(part)) + return false; + *m = map[part]; + return true; +} + +// When the cache size exceeds 1MB, it clears out about 1/2 of the cache. +void QCompletionEngine::saveInCache(QString part, const QModelIndex& parent, const QMatchData& m) +{ + QMatchData old = cache[parent].take(part); + cost = cost + m.indices.cost() - old.indices.cost(); + if (cost * sizeof(int) > 1024 * 1024) { + QMap<QModelIndex, CacheItem>::iterator it1 = cache.begin(); + while (it1 != cache.end()) { + CacheItem& ci = it1.value(); + int sz = ci.count()/2; + QMap<QString, QMatchData>::iterator it2 = ci.begin(); + int i = 0; + while (it2 != ci.end() && i < sz) { + cost -= it2.value().indices.cost(); + it2 = ci.erase(it2); + i++; + } + if (ci.count() == 0) { + it1 = cache.erase(it1); + } else { + ++it1; + } + } + } + + if (c->cs == Qt::CaseInsensitive) + part = part.toLower(); + cache[parent][part] = m; +} + +/////////////////////////////////////////////////////////////////////////////////// +QIndexMapper QSortedModelEngine::indexHint(QString part, const QModelIndex& parent, Qt::SortOrder order) +{ + const QAbstractItemModel *model = c->proxy->sourceModel(); + + if (c->cs == Qt::CaseInsensitive) + part = part.toLower(); + + const CacheItem& map = cache[parent]; + + // Try to find a lower and upper bound for the search from previous results + int to = model->rowCount(parent) - 1; + int from = 0; + const CacheItem::const_iterator it = map.lowerBound(part); + + // look backward for first valid hint + for(CacheItem::const_iterator it1 = it; it1-- != map.constBegin();) { + const QMatchData& value = it1.value(); + if (value.isValid()) { + if (order == Qt::AscendingOrder) { + from = value.indices.last() + 1; + } else { + to = value.indices.first() - 1; + } + break; + } + } + + // look forward for first valid hint + for(CacheItem::const_iterator it2 = it; it2 != map.constEnd(); ++it2) { + const QMatchData& value = it2.value(); + if (value.isValid() && !it2.key().startsWith(part)) { + if (order == Qt::AscendingOrder) { + to = value.indices.first() - 1; + } else { + from = value.indices.first() + 1; + } + break; + } + } + + return QIndexMapper(from, to); +} + +Qt::SortOrder QSortedModelEngine::sortOrder(const QModelIndex &parent) const +{ + const QAbstractItemModel *model = c->proxy->sourceModel(); + + int rowCount = model->rowCount(parent); + if (rowCount < 2) + return Qt::AscendingOrder; + QString first = model->data(model->index(0, c->column, parent), c->role).toString(); + QString last = model->data(model->index(rowCount - 1, c->column, parent), c->role).toString(); + return QString::compare(first, last, c->cs) <= 0 ? Qt::AscendingOrder : Qt::DescendingOrder; +} + +QMatchData QSortedModelEngine::filter(const QString& part, const QModelIndex& parent, int) +{ + const QAbstractItemModel *model = c->proxy->sourceModel(); + + QMatchData hint; + if (lookupCache(part, parent, &hint)) + return hint; + + QIndexMapper indices; + Qt::SortOrder order = sortOrder(parent); + + if (matchHint(part, parent, &hint)) { + if (!hint.isValid()) + return QMatchData(); + indices = hint.indices; + } else { + indices = indexHint(part, parent, order); + } + + // binary search the model within 'indices' for 'part' under 'parent' + int high = indices.to() + 1; + int low = indices.from() - 1; + int probe; + QModelIndex probeIndex; + QString probeData; + + while (high - low > 1) + { + probe = (high + low) / 2; + probeIndex = model->index(probe, c->column, parent); + probeData = model->data(probeIndex, c->role).toString(); + const int cmp = QString::compare(probeData, part, c->cs); + if ((order == Qt::AscendingOrder && cmp >= 0) + || (order == Qt::DescendingOrder && cmp < 0)) { + high = probe; + } else { + low = probe; + } + } + + if ((order == Qt::AscendingOrder && low == indices.to()) + || (order == Qt::DescendingOrder && high == indices.from())) { // not found + saveInCache(part, parent, QMatchData()); + return QMatchData(); + } + + probeIndex = model->index(order == Qt::AscendingOrder ? low+1 : high-1, c->column, parent); + probeData = model->data(probeIndex, c->role).toString(); + if (!probeData.startsWith(part, c->cs)) { + saveInCache(part, parent, QMatchData()); + return QMatchData(); + } + + const bool exactMatch = QString::compare(probeData, part, c->cs) == 0; + int emi = exactMatch ? (order == Qt::AscendingOrder ? low+1 : high-1) : -1; + + int from = 0; + int to = 0; + if (order == Qt::AscendingOrder) { + from = low + 1; + high = indices.to() + 1; + low = from; + } else { + to = high - 1; + low = indices.from() - 1; + high = to; + } + + while (high - low > 1) + { + probe = (high + low) / 2; + probeIndex = model->index(probe, c->column, parent); + probeData = model->data(probeIndex, c->role).toString(); + const bool startsWith = probeData.startsWith(part, c->cs); + if ((order == Qt::AscendingOrder && startsWith) + || (order == Qt::DescendingOrder && !startsWith)) { + low = probe; + } else { + high = probe; + } + } + + QMatchData m(order == Qt::AscendingOrder ? QIndexMapper(from, high - 1) : QIndexMapper(low+1, to), emi, false); + saveInCache(part, parent, m); + return m; +} + +//////////////////////////////////////////////////////////////////////////////////////// +int QUnsortedModelEngine::buildIndices(const QString& str, const QModelIndex& parent, int n, + const QIndexMapper& indices, QMatchData* m) +{ + Q_ASSERT(m->partial); + Q_ASSERT(n != -1 || m->exactMatchIndex == -1); + const QAbstractItemModel *model = c->proxy->sourceModel(); + int i, count = 0; + + for (i = 0; i < indices.count() && count != n; ++i) { + QModelIndex idx = model->index(indices[i], c->column, parent); + QString data = model->data(idx, c->role).toString(); + if (!data.startsWith(str, c->cs) || !(model->flags(idx) & Qt::ItemIsSelectable)) + continue; + m->indices.append(indices[i]); + ++count; + if (m->exactMatchIndex == -1 && QString::compare(data, str, c->cs) == 0) { + m->exactMatchIndex = indices[i]; + if (n == -1) + return indices[i]; + } + } + return indices[i-1]; +} + +void QUnsortedModelEngine::filterOnDemand(int n) +{ + Q_ASSERT(matchCount()); + if (!curMatch.partial) + return; + Q_ASSERT(n >= -1); + const QAbstractItemModel *model = c->proxy->sourceModel(); + int lastRow = model->rowCount(curParent) - 1; + QIndexMapper im(curMatch.indices.last() + 1, lastRow); + int lastIndex = buildIndices(curParts.last(), curParent, n, im, &curMatch); + curMatch.partial = (lastRow != lastIndex); + saveInCache(curParts.last(), curParent, curMatch); +} + +QMatchData QUnsortedModelEngine::filter(const QString& part, const QModelIndex& parent, int n) +{ + QMatchData hint; + + QVector<int> v; + QIndexMapper im(v); + QMatchData m(im, -1, true); + + const QAbstractItemModel *model = c->proxy->sourceModel(); + bool foundInCache = lookupCache(part, parent, &m); + + if (!foundInCache) { + if (matchHint(part, parent, &hint) && !hint.isValid()) + return QMatchData(); + } + + if (!foundInCache && !hint.isValid()) { + const int lastRow = model->rowCount(parent) - 1; + QIndexMapper all(0, lastRow); + int lastIndex = buildIndices(part, parent, n, all, &m); + m.partial = (lastIndex != lastRow); + } else { + if (!foundInCache) { // build from hint as much as we can + buildIndices(part, parent, INT_MAX, hint.indices, &m); + m.partial = hint.partial; + } + if (m.partial && ((n == -1 && m.exactMatchIndex == -1) || (m.indices.count() < n))) { + // need more and have more + const int lastRow = model->rowCount(parent) - 1; + QIndexMapper rest(hint.indices.last() + 1, lastRow); + int want = n == -1 ? -1 : n - m.indices.count(); + int lastIndex = buildIndices(part, parent, want, rest, &m); + m.partial = (lastRow != lastIndex); + } + } + + saveInCache(part, parent, m); + return m; +} + +/////////////////////////////////////////////////////////////////////////////// +QCompleterPrivate::QCompleterPrivate() +: widget(0), proxy(0), popup(0), cs(Qt::CaseSensitive), role(Qt::EditRole), column(0), + maxVisibleItems(7), sorting(QCompleter::UnsortedModel), wrap(true), eatFocusOut(true), + hiddenBecauseNoMatch(false) +{ +} + +void QCompleterPrivate::init(QAbstractItemModel *m) +{ + Q_Q(QCompleter); + proxy = new QCompletionModel(this, q); + QObject::connect(proxy, SIGNAL(rowsAdded()), q, SLOT(_q_autoResizePopup())); + q->setModel(m); +#ifdef QT_NO_LISTVIEW + q->setCompletionMode(QCompleter::InlineCompletion); +#else + q->setCompletionMode(QCompleter::PopupCompletion); +#endif // QT_NO_LISTVIEW +} + +void QCompleterPrivate::setCurrentIndex(QModelIndex index, bool select) +{ + Q_Q(QCompleter); + if (!q->popup()) + return; + if (!select) { + popup->selectionModel()->setCurrentIndex(index, QItemSelectionModel::NoUpdate); + } else { + if (!index.isValid()) + popup->selectionModel()->clear(); + else + popup->selectionModel()->setCurrentIndex(index, QItemSelectionModel::Select + | QItemSelectionModel::Rows); + } + index = popup->selectionModel()->currentIndex(); + if (!index.isValid()) + popup->scrollToTop(); + else + popup->scrollTo(index, QAbstractItemView::PositionAtTop); +} + +void QCompleterPrivate::_q_completionSelected(const QItemSelection& selection) +{ + QModelIndex index; + if (!selection.indexes().isEmpty()) + index = selection.indexes().first(); + + _q_complete(index, true); +} + +void QCompleterPrivate::_q_complete(QModelIndex index, bool highlighted) +{ + Q_Q(QCompleter); + QString completion; + + if (!index.isValid() || (!proxy->showAll && (index.row() >= proxy->engine->matchCount()))) { + completion = prefix; + } else { + if (!(index.flags() & Qt::ItemIsEnabled)) + return; + QModelIndex si = proxy->mapToSource(index); + si = si.sibling(si.row(), column); // for clicked() + completion = q->pathFromIndex(si); +#ifndef QT_NO_DIRMODEL + // add a trailing separator in inline + if (mode == QCompleter::InlineCompletion) { + if (qobject_cast<QDirModel *>(proxy->sourceModel()) && QFileInfo(completion).isDir()) + completion += QDir::separator(); + } +#endif +#ifndef QT_NO_FILESYSTEMMODEL + // add a trailing separator in inline + if (mode == QCompleter::InlineCompletion) { + if (qobject_cast<QFileSystemModel *>(proxy->sourceModel()) && QFileInfo(completion).isDir()) + completion += QDir::separator(); + } +#endif + } + + if (highlighted) { + emit q->highlighted(index); + emit q->highlighted(completion); + } else { + emit q->activated(index); + emit q->activated(completion); + } +} + +void QCompleterPrivate::_q_autoResizePopup() +{ + if (!popup || !popup->isVisible()) + return; + showPopup(popupRect); +} + +void QCompleterPrivate::showPopup(const QRect& rect) +{ + const QRect screen = QApplication::desktop()->availableGeometry(widget); + Qt::LayoutDirection dir = widget->layoutDirection(); + QPoint pos; + int rh, w; + int h = (popup->sizeHintForRow(0) * qMin(maxVisibleItems, popup->model()->rowCount()) + 3) + 3; + QScrollBar *hsb = popup->horizontalScrollBar(); + if (hsb && hsb->isVisible()) + h += popup->horizontalScrollBar()->sizeHint().height(); + + if (rect.isValid()) { + rh = rect.height(); + w = rect.width(); + pos = widget->mapToGlobal(dir == Qt::RightToLeft ? rect.bottomRight() : rect.bottomLeft()); + } else { + rh = widget->height(); + pos = widget->mapToGlobal(QPoint(0, widget->height() - 2)); + w = widget->width(); + } + + if (w > screen.width()) + w = screen.width(); + if ((pos.x() + w) > (screen.x() + screen.width())) + pos.setX(screen.x() + screen.width() - w); + if (pos.x() < screen.x()) + pos.setX(screen.x()); + + int top = pos.y() - rh - screen.top() + 2; + int bottom = screen.bottom() - pos.y(); + h = qMax(h, popup->minimumHeight()); + if (h > bottom) { + h = qMin(qMax(top, bottom), h); + + if (top > bottom) + pos.setY(pos.y() - h - rh + 2); + } + + popup->setGeometry(pos.x(), pos.y(), w, h); + + if (!popup->isVisible()) + popup->show(); +} + +void QCompleterPrivate::_q_fileSystemModelDirectoryLoaded(const QString &path) +{ + Q_Q(QCompleter); + // Slot called when QFileSystemModel has finished loading. + // If we hide the popup because there was no match because the model was not loaded yet, + // we re-start the completion when we get the results + if (hiddenBecauseNoMatch + && prefix.startsWith(path) && prefix != (path + QLatin1Char('/')) + && widget) { + q->complete(); + } +} + +/*! + Constructs a completer object with the given \a parent. +*/ +QCompleter::QCompleter(QObject *parent) +: QObject(*new QCompleterPrivate(), parent) +{ + Q_D(QCompleter); + d->init(); +} + +/*! + Constructs a completer object with the given \a parent that provides completions + from the specified \a model. +*/ +QCompleter::QCompleter(QAbstractItemModel *model, QObject *parent) + : QObject(*new QCompleterPrivate(), parent) +{ + Q_D(QCompleter); + d->init(model); +} + +#ifndef QT_NO_STRINGLISTMODEL +/*! + Constructs a QCompleter object with the given \a parent that uses the specified + \a list as a source of possible completions. +*/ +QCompleter::QCompleter(const QStringList& list, QObject *parent) +: QObject(*new QCompleterPrivate(), parent) +{ + Q_D(QCompleter); + d->init(new QStringListModel(list, this)); +} +#endif // QT_NO_STRINGLISTMODEL + +/*! + Destroys the completer object. +*/ +QCompleter::~QCompleter() +{ +} + +/*! + Sets the widget for which completion are provided for to \a widget. This + function is automatically called when a QCompleter is set on a QLineEdit + using QLineEdit::setCompleter() or on a QComboBox using + QComboBox::setCompleter(). The widget needs to be set explicitly when + providing completions for custom widgets. + + \sa widget(), setModel(), setPopup() + */ +void QCompleter::setWidget(QWidget *widget) +{ + Q_D(QCompleter); + if (d->widget) + d->widget->removeEventFilter(this); + d->widget = widget; + if (d->widget) + d->widget->installEventFilter(this); + if (d->popup) { + d->popup->hide(); + d->popup->setFocusProxy(d->widget); + } +} + +/*! + Returns the widget for which the completer object is providing completions. + + \sa setWidget() + */ +QWidget *QCompleter::widget() const +{ + Q_D(const QCompleter); + return d->widget; +} + +/*! + Sets the model which provides completions to \a model. The \a model can + be list model or a tree model. If a model has been already previously set + and it has the QCompleter as its parent, it is deleted. + + For convenience, if \a model is a QFileSystemModel, QCompleter switches its + caseSensitivity to Qt::CaseInsensitive on Windows and Qt::CaseSensitive + on other platforms. + + \sa completionModel(), modelSorting, {Handling Tree Models} +*/ +void QCompleter::setModel(QAbstractItemModel *model) +{ + Q_D(QCompleter); + QAbstractItemModel *oldModel = d->proxy->sourceModel(); + d->proxy->setSourceModel(model); + if (d->popup) + setPopup(d->popup); // set the model and make new connections + if (oldModel && oldModel->QObject::parent() == this) + delete oldModel; +#ifndef QT_NO_DIRMODEL + if (qobject_cast<QDirModel *>(model)) { +#if (defined(Q_OS_WIN) && !defined(Q_OS_WINCE)) || defined(Q_OS_SYMBIAN) + setCaseSensitivity(Qt::CaseInsensitive); +#else + setCaseSensitivity(Qt::CaseSensitive); +#endif + } +#endif // QT_NO_DIRMODEL +#ifndef QT_NO_FILESYSTEMMODEL + QFileSystemModel *fsModel = qobject_cast<QFileSystemModel *>(model); + if (fsModel) { +#if (defined(Q_OS_WIN) && !defined(Q_OS_WINCE)) || defined(Q_OS_SYMBIAN) + setCaseSensitivity(Qt::CaseInsensitive); +#else + setCaseSensitivity(Qt::CaseSensitive); +#endif + setCompletionRole(QFileSystemModel::FileNameRole); + connect(fsModel, SIGNAL(directoryLoaded(QString)), this, SLOT(_q_fileSystemModelDirectoryLoaded(QString))); + } +#endif // QT_NO_FILESYSTEMMODEL +} + +/*! + Returns the model that provides completion strings. + + \sa completionModel() +*/ +QAbstractItemModel *QCompleter::model() const +{ + Q_D(const QCompleter); + return d->proxy->sourceModel(); +} + +/*! + \enum QCompleter::CompletionMode + + This enum specifies how completions are provided to the user. + + \value PopupCompletion Current completions are displayed in a popup window. + \value InlineCompletion Completions appear inline (as selected text). + \value UnfilteredPopupCompletion All possible completions are displayed in a popup window with the most likely suggestion indicated as current. + + \sa setCompletionMode() +*/ + +/*! + \property QCompleter::completionMode + \brief how the completions are provided to the user + + The default value is QCompleter::PopupCompletion. +*/ +void QCompleter::setCompletionMode(QCompleter::CompletionMode mode) +{ + Q_D(QCompleter); + d->mode = mode; + d->proxy->setFiltered(mode != QCompleter::UnfilteredPopupCompletion); + + if (mode == QCompleter::InlineCompletion) { + if (d->widget) + d->widget->removeEventFilter(this); + if (d->popup) { + d->popup->deleteLater(); + d->popup = 0; + } + } else { + if (d->widget) + d->widget->installEventFilter(this); + } +} + +QCompleter::CompletionMode QCompleter::completionMode() const +{ + Q_D(const QCompleter); + return d->mode; +} + +/*! + Sets the popup used to display completions to \a popup. QCompleter takes + ownership of the view. + + A QListView is automatically created when the completionMode() is set to + QCompleter::PopupCompletion or QCompleter::UnfilteredPopupCompletion. The + default popup displays the completionColumn(). + + Ensure that this function is called before the view settings are modified. + This is required since view's properties may require that a model has been + set on the view (for example, hiding columns in the view requires a model + to be set on the view). + + \sa popup() +*/ +void QCompleter::setPopup(QAbstractItemView *popup) +{ + Q_D(QCompleter); + Q_ASSERT(popup != 0); + if (d->popup) { + QObject::disconnect(d->popup->selectionModel(), 0, this, 0); + QObject::disconnect(d->popup, 0, this, 0); + } + if (d->popup != popup) + delete d->popup; + if (popup->model() != d->proxy) + popup->setModel(d->proxy); +#if defined(Q_OS_MAC) && !defined(QT_MAC_USE_COCOA) + popup->show(); +#else + popup->hide(); +#endif + + Qt::FocusPolicy origPolicy = Qt::NoFocus; + if (d->widget) + origPolicy = d->widget->focusPolicy(); + popup->setParent(0, Qt::Popup); + popup->setFocusPolicy(Qt::NoFocus); + if (d->widget) + d->widget->setFocusPolicy(origPolicy); + + popup->setFocusProxy(d->widget); + popup->installEventFilter(this); + popup->setItemDelegate(new QCompleterItemDelegate(popup)); +#ifndef QT_NO_LISTVIEW + if (QListView *listView = qobject_cast<QListView *>(popup)) { + listView->setModelColumn(d->column); + } +#endif + + QObject::connect(popup, SIGNAL(clicked(QModelIndex)), + this, SLOT(_q_complete(QModelIndex))); + QObject::connect(this, SIGNAL(activated(QModelIndex)), + popup, SLOT(hide())); + + QObject::connect(popup->selectionModel(), SIGNAL(selectionChanged(QItemSelection,QItemSelection)), + this, SLOT(_q_completionSelected(QItemSelection))); + d->popup = popup; +} + +/*! + Returns the popup used to display completions. + + \sa setPopup() +*/ +QAbstractItemView *QCompleter::popup() const +{ + Q_D(const QCompleter); +#ifndef QT_NO_LISTVIEW + if (!d->popup && completionMode() != QCompleter::InlineCompletion) { + QListView *listView = new QListView; + listView->setEditTriggers(QAbstractItemView::NoEditTriggers); + listView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + listView->setSelectionBehavior(QAbstractItemView::SelectRows); + listView->setSelectionMode(QAbstractItemView::SingleSelection); + listView->setModelColumn(d->column); + QCompleter *that = const_cast<QCompleter*>(this); + that->setPopup(listView); + } +#endif // QT_NO_LISTVIEW + return d->popup; +} + +/*! + \reimp +*/ +bool QCompleter::event(QEvent *ev) +{ + return QObject::event(ev); +} + +/*! + \reimp +*/ +bool QCompleter::eventFilter(QObject *o, QEvent *e) +{ + Q_D(QCompleter); + + if (d->eatFocusOut && o == d->widget && e->type() == QEvent::FocusOut) { + d->hiddenBecauseNoMatch = false; + if (d->popup && d->popup->isVisible()) + return true; + } + + if (o != d->popup) + return QObject::eventFilter(o, e); + + switch (e->type()) { + case QEvent::KeyPress: { + QKeyEvent *ke = static_cast<QKeyEvent *>(e); + + QModelIndex curIndex = d->popup->currentIndex(); + QModelIndexList selList = d->popup->selectionModel()->selectedIndexes(); + + const int key = ke->key(); + // In UnFilteredPopup mode, select the current item + if ((key == Qt::Key_Up || key == Qt::Key_Down) && selList.isEmpty() && curIndex.isValid() + && d->mode == QCompleter::UnfilteredPopupCompletion) { + d->setCurrentIndex(curIndex); + return true; + } + + // Handle popup navigation keys. These are hardcoded because up/down might make the + // widget do something else (lineedit cursor moves to home/end on mac, for instance) + switch (key) { + case Qt::Key_End: + case Qt::Key_Home: + if (ke->modifiers() & Qt::ControlModifier) + return false; + break; + + case Qt::Key_Up: + if (!curIndex.isValid()) { + int rowCount = d->proxy->rowCount(); + QModelIndex lastIndex = d->proxy->index(rowCount - 1, d->column); + d->setCurrentIndex(lastIndex); + return true; + } else if (curIndex.row() == 0) { + if (d->wrap) + d->setCurrentIndex(QModelIndex()); + return true; + } + return false; + + case Qt::Key_Down: + if (!curIndex.isValid()) { + QModelIndex firstIndex = d->proxy->index(0, d->column); + d->setCurrentIndex(firstIndex); + return true; + } else if (curIndex.row() == d->proxy->rowCount() - 1) { + if (d->wrap) + d->setCurrentIndex(QModelIndex()); + return true; + } + return false; + + case Qt::Key_PageUp: + case Qt::Key_PageDown: + return false; + } + + // Send the event to the widget. If the widget accepted the event, do nothing + // If the widget did not accept the event, provide a default implementation + d->eatFocusOut = false; + (static_cast<QObject *>(d->widget))->event(ke); + d->eatFocusOut = true; + if (!d->widget || e->isAccepted() || !d->popup->isVisible()) { + // widget lost focus, hide the popup + if (d->widget && (!d->widget->hasFocus() +#ifdef QT_KEYPAD_NAVIGATION + || (QApplication::keypadNavigationEnabled() && !d->widget->hasEditFocus()) +#endif + )) + d->popup->hide(); + if (e->isAccepted()) + return true; + } + + // default implementation for keys not handled by the widget when popup is open + switch (key) { +#ifdef QT_KEYPAD_NAVIGATION + case Qt::Key_Select: + if (!QApplication::keypadNavigationEnabled()) + break; +#endif + case Qt::Key_Return: + case Qt::Key_Enter: + case Qt::Key_Tab: + d->popup->hide(); + if (curIndex.isValid()) + d->_q_complete(curIndex); + break; + + case Qt::Key_F4: + if (ke->modifiers() & Qt::AltModifier) + d->popup->hide(); + break; + + case Qt::Key_Backtab: + case Qt::Key_Escape: + d->popup->hide(); + break; + + default: + break; + } + + return true; + } + +#ifdef QT_KEYPAD_NAVIGATION + case QEvent::KeyRelease: { + QKeyEvent *ke = static_cast<QKeyEvent *>(e); + if (QApplication::keypadNavigationEnabled() && ke->key() == Qt::Key_Back) { + // Send the event to the 'widget'. This is what we did for KeyPress, so we need + // to do the same for KeyRelease, in case the widget's KeyPress event set + // up something (such as a timer) that is relying on also receiving the + // key release. I see this as a bug in Qt, and should really set it up for all + // the affected keys. However, it is difficult to tell how this will affect + // existing code, and I can't test for every combination! + d->eatFocusOut = false; + static_cast<QObject *>(d->widget)->event(ke); + d->eatFocusOut = true; + } + break; + } +#endif + + case QEvent::MouseButtonPress: { +#ifdef QT_KEYPAD_NAVIGATION + if (QApplication::keypadNavigationEnabled()) { + // if we've clicked in the widget (or its descendant), let it handle the click + QWidget *source = qobject_cast<QWidget *>(o); + if (source) { + QPoint pos = source->mapToGlobal((static_cast<QMouseEvent *>(e))->pos()); + QWidget *target = QApplication::widgetAt(pos); + if (target && (d->widget->isAncestorOf(target) || + target == d->widget)) { + d->eatFocusOut = false; + static_cast<QObject *>(target)->event(e); + d->eatFocusOut = true; + return true; + } + } + } +#endif + if (!d->popup->underMouse()) { + d->popup->hide(); + return true; + } + } + return false; + + case QEvent::InputMethod: + case QEvent::ShortcutOverride: + QApplication::sendEvent(d->widget, e); + break; + + default: + return false; + } + return false; +} + +/*! + For QCompleter::PopupCompletion and QCompletion::UnfilteredPopupCompletion + modes, calling this function displays the popup displaying the current + completions. By default, if \a rect is not specified, the popup is displayed + on the bottom of the widget(). If \a rect is specified the popup is + displayed on the left edge of the rectangle. + + For QCompleter::InlineCompletion mode, the highlighted() signal is fired + with the current completion. +*/ +void QCompleter::complete(const QRect& rect) +{ + Q_D(QCompleter); + QModelIndex idx = d->proxy->currentIndex(false); + d->hiddenBecauseNoMatch = false; + if (d->mode == QCompleter::InlineCompletion) { + if (idx.isValid()) + d->_q_complete(idx, true); + return; + } + + Q_ASSERT(d->widget != 0); + if ((d->mode == QCompleter::PopupCompletion && !idx.isValid()) + || (d->mode == QCompleter::UnfilteredPopupCompletion && d->proxy->rowCount() == 0)) { + if (d->popup) + d->popup->hide(); // no suggestion, hide + d->hiddenBecauseNoMatch = true; + return; + } + + popup(); + if (d->mode == QCompleter::UnfilteredPopupCompletion) + d->setCurrentIndex(idx, false); + + d->showPopup(rect); + d->popupRect = rect; +} + +/*! + Sets the current row to the \a row specified. Returns true if successful; + otherwise returns false. + + This function may be used along with currentCompletion() to iterate + through all the possible completions. + + \sa currentCompletion(), completionCount() +*/ +bool QCompleter::setCurrentRow(int row) +{ + Q_D(QCompleter); + return d->proxy->setCurrentRow(row); +} + +/*! + Returns the current row. + + \sa setCurrentRow() +*/ +int QCompleter::currentRow() const +{ + Q_D(const QCompleter); + return d->proxy->currentRow(); +} + +/*! + Returns the number of completions for the current prefix. For an unsorted + model with a large number of items this can be expensive. Use setCurrentRow() + and currentCompletion() to iterate through all the completions. +*/ +int QCompleter::completionCount() const +{ + Q_D(const QCompleter); + return d->proxy->completionCount(); +} + +/*! + \enum QCompleter::ModelSorting + + This enum specifies how the items in the model are sorted. + + \value UnsortedModel The model is unsorted. + \value CaseSensitivelySortedModel The model is sorted case sensitively. + \value CaseInsensitivelySortedModel The model is sorted case insensitively. + + \sa setModelSorting() +*/ + +/*! + \property QCompleter::modelSorting + \brief the way the model is sorted + + By default, no assumptions are made about the order of the items + in the model that provides the completions. + + If the model's data for the completionColumn() and completionRole() is sorted in + ascending order, you can set this property to \l CaseSensitivelySortedModel + or \l CaseInsensitivelySortedModel. On large models, this can lead to + significant performance improvements because the completer object can + then use a binary search algorithm instead of linear search algorithm. + + The sort order (i.e ascending or descending order) of the model is determined + dynamically by inspecting the contents of the model. + + \bold{Note:} The performance improvements described above cannot take place + when the completer's \l caseSensitivity is different to the case sensitivity + used by the model's when sorting. + + \sa setCaseSensitivity(), QCompleter::ModelSorting +*/ +void QCompleter::setModelSorting(QCompleter::ModelSorting sorting) +{ + Q_D(QCompleter); + if (d->sorting == sorting) + return; + d->sorting = sorting; + d->proxy->createEngine(); + d->proxy->invalidate(); +} + +QCompleter::ModelSorting QCompleter::modelSorting() const +{ + Q_D(const QCompleter); + return d->sorting; +} + +/*! + \property QCompleter::completionColumn + \brief the column in the model in which completions are searched for. + + If the popup() is a QListView, it is automatically setup to display + this column. + + By default, the match column is 0. + + \sa completionRole, caseSensitivity +*/ +void QCompleter::setCompletionColumn(int column) +{ + Q_D(QCompleter); + if (d->column == column) + return; +#ifndef QT_NO_LISTVIEW + if (QListView *listView = qobject_cast<QListView *>(d->popup)) + listView->setModelColumn(column); +#endif + d->column = column; + d->proxy->invalidate(); +} + +int QCompleter::completionColumn() const +{ + Q_D(const QCompleter); + return d->column; +} + +/*! + \property QCompleter::completionRole + \brief the item role to be used to query the contents of items for matching. + + The default role is Qt::EditRole. + + \sa completionColumn, caseSensitivity +*/ +void QCompleter::setCompletionRole(int role) +{ + Q_D(QCompleter); + if (d->role == role) + return; + d->role = role; + d->proxy->invalidate(); +} + +int QCompleter::completionRole() const +{ + Q_D(const QCompleter); + return d->role; +} + +/*! + \property QCompleter::wrapAround + \brief the completions wrap around when navigating through items + \since 4.3 + + The default is true. +*/ +void QCompleter::setWrapAround(bool wrap) +{ + Q_D(QCompleter); + if (d->wrap == wrap) + return; + d->wrap = wrap; +} + +bool QCompleter::wrapAround() const +{ + Q_D(const QCompleter); + return d->wrap; +} + +/*! + \property QCompleter::maxVisibleItems + \brief the maximum allowed size on screen of the completer, measured in items + \since 4.6 + + By default, this property has a value of 7. +*/ +int QCompleter::maxVisibleItems() const +{ + Q_D(const QCompleter); + return d->maxVisibleItems; +} + +void QCompleter::setMaxVisibleItems(int maxItems) +{ + Q_D(QCompleter); + if (maxItems < 0) { + qWarning("QCompleter::setMaxVisibleItems: " + "Invalid max visible items (%d) must be >= 0", maxItems); + return; + } + d->maxVisibleItems = maxItems; +} + +/*! + \property QCompleter::caseSensitivity + \brief the case sensitivity of the matching + + The default is Qt::CaseSensitive. + + \sa completionColumn, completionRole, modelSorting +*/ +void QCompleter::setCaseSensitivity(Qt::CaseSensitivity cs) +{ + Q_D(QCompleter); + if (d->cs == cs) + return; + d->cs = cs; + d->proxy->createEngine(); + d->proxy->invalidate(); +} + +Qt::CaseSensitivity QCompleter::caseSensitivity() const +{ + Q_D(const QCompleter); + return d->cs; +} + +/*! + \property QCompleter::completionPrefix + \brief the completion prefix used to provide completions. + + The completionModel() is updated to reflect the list of possible + matches for \a prefix. +*/ +void QCompleter::setCompletionPrefix(const QString &prefix) +{ + Q_D(QCompleter); + d->prefix = prefix; + d->proxy->filter(splitPath(prefix)); +} + +QString QCompleter::completionPrefix() const +{ + Q_D(const QCompleter); + return d->prefix; +} + +/*! + Returns the model index of the current completion in the completionModel(). + + \sa setCurrentRow(), currentCompletion(), model() +*/ +QModelIndex QCompleter::currentIndex() const +{ + Q_D(const QCompleter); + return d->proxy->currentIndex(false); +} + +/*! + Returns the current completion string. This includes the \l completionPrefix. + When used alongside setCurrentRow(), it can be used to iterate through + all the matches. + + \sa setCurrentRow(), currentIndex() +*/ +QString QCompleter::currentCompletion() const +{ + Q_D(const QCompleter); + return pathFromIndex(d->proxy->currentIndex(true)); +} + +/*! + Returns the completion model. The completion model is a read-only list model + that contains all the possible matches for the current completion prefix. + The completion model is auto-updated to reflect the current completions. + + \note The return value of this function is defined to be an QAbstractItemModel + purely for generality. This actual kind of model returned is an instance of an + QAbstractProxyModel subclass. + + \sa completionPrefix, model() +*/ +QAbstractItemModel *QCompleter::completionModel() const +{ + Q_D(const QCompleter); + return d->proxy; +} + +/*! + Returns the path for the given \a index. The completer object uses this to + obtain the completion text from the underlying model. + + The default implementation returns the \l{Qt::EditRole}{edit role} of the + item for list models. It returns the absolute file path if the model is a + QFileSystemModel. + + \sa splitPath() +*/ + +QString QCompleter::pathFromIndex(const QModelIndex& index) const +{ + Q_D(const QCompleter); + if (!index.isValid()) + return QString(); + + QAbstractItemModel *sourceModel = d->proxy->sourceModel(); + if (!sourceModel) + return QString(); + bool isDirModel = false; + bool isFsModel = false; +#ifndef QT_NO_DIRMODEL + isDirModel = qobject_cast<QDirModel *>(d->proxy->sourceModel()) != 0; +#endif +#ifndef QT_NO_FILESYSTEMMODEL + isFsModel = qobject_cast<QFileSystemModel *>(d->proxy->sourceModel()) != 0; +#endif + if (!isDirModel && !isFsModel) + return sourceModel->data(index, d->role).toString(); + + QModelIndex idx = index; + QStringList list; + do { + QString t; + if (isDirModel) + t = sourceModel->data(idx, Qt::EditRole).toString(); +#ifndef QT_NO_FILESYSTEMMODEL + else + t = sourceModel->data(idx, QFileSystemModel::FileNameRole).toString(); +#endif + list.prepend(t); + QModelIndex parent = idx.parent(); + idx = parent.sibling(parent.row(), index.column()); + } while (idx.isValid()); + +#if (!defined(Q_OS_WIN) || defined(Q_OS_WINCE)) && !defined(Q_OS_SYMBIAN) + if (list.count() == 1) // only the separator or some other text + return list[0]; + list[0].clear() ; // the join below will provide the separator +#endif + + return list.join(QDir::separator()); +} + +/*! + Splits the given \a path into strings that are used to match at each level + in the model(). + + The default implementation of splitPath() splits a file system path based on + QDir::separator() when the sourceModel() is a QFileSystemModel. + + When used with list models, the first item in the returned list is used for + matching. + + \sa pathFromIndex(), {Handling Tree Models} +*/ +QStringList QCompleter::splitPath(const QString& path) const +{ + bool isDirModel = false; + bool isFsModel = false; +#ifndef QT_NO_DIRMODEL + Q_D(const QCompleter); + isDirModel = qobject_cast<QDirModel *>(d->proxy->sourceModel()) != 0; +#endif +#ifndef QT_NO_FILESYSTEMMODEL +#ifdef QT_NO_DIRMODEL + Q_D(const QCompleter); +#endif + isFsModel = qobject_cast<QFileSystemModel *>(d->proxy->sourceModel()) != 0; +#endif + + if ((!isDirModel && !isFsModel) || path.isEmpty()) + return QStringList(completionPrefix()); + + QString pathCopy = QDir::toNativeSeparators(path); + QString sep = QDir::separator(); +#if defined(Q_OS_SYMBIAN) + if (pathCopy == QLatin1String("\\")) + return QStringList(pathCopy); +#elif defined(Q_OS_WIN) && !defined(Q_OS_WINCE) + if (pathCopy == QLatin1String("\\") || pathCopy == QLatin1String("\\\\")) + return QStringList(pathCopy); + QString doubleSlash(QLatin1String("\\\\")); + if (pathCopy.startsWith(doubleSlash)) + pathCopy = pathCopy.mid(2); + else + doubleSlash.clear(); +#endif + + QRegExp re(QLatin1Char('[') + QRegExp::escape(sep) + QLatin1Char(']')); + QStringList parts = pathCopy.split(re); + +#if defined(Q_OS_SYMBIAN) + // Do nothing +#elif defined(Q_OS_WIN) && !defined(Q_OS_WINCE) + if (!doubleSlash.isEmpty()) + parts[0].prepend(doubleSlash); +#else + if (pathCopy[0] == sep[0]) // readd the "/" at the beginning as the split removed it + parts[0] = QDir::fromNativeSeparators(QString(sep[0])); +#endif + + return parts; +} + +/*! + \fn void QCompleter::activated(const QModelIndex& index) + + This signal is sent when an item in the popup() is activated by the user. + (by clicking or pressing return). The item's \a index in the completionModel() + is given. + +*/ + +/*! + \fn void QCompleter::activated(const QString &text) + + This signal is sent when an item in the popup() is activated by the user (by + clicking or pressing return). The item's \a text is given. + +*/ + +/*! + \fn void QCompleter::highlighted(const QModelIndex& index) + + This signal is sent when an item in the popup() is highlighted by + the user. It is also sent if complete() is called with the completionMode() + set to QCompleter::InlineCompletion. The item's \a index in the completionModel() + is given. +*/ + +/*! + \fn void QCompleter::highlighted(const QString &text) + + This signal is sent when an item in the popup() is highlighted by + the user. It is also sent if complete() is called with the completionMode() + set to QCompleter::InlineCompletion. The item's \a text is given. +*/ + +QT_END_NAMESPACE + +#include "moc_qcompleter.cpp" + +#endif // QT_NO_COMPLETER diff --git a/src/widgets/util/qcompleter.h b/src/widgets/util/qcompleter.h new file mode 100644 index 0000000000..ace2f0afb6 --- /dev/null +++ b/src/widgets/util/qcompleter.h @@ -0,0 +1,171 @@ +/**************************************************************************** +** +** Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies). +** All rights reserved. +** Contact: Nokia Corporation (qt-info@nokia.com) +** +** This file is part of the QtGui module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** No Commercial Usage +** This file contains pre-release code and may not be distributed. +** You may use this file in accordance with the terms and conditions +** contained in the Technology Preview License Agreement accompanying +** this package. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain additional +** rights. These rights are described in the Nokia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** If you have questions regarding the use of this file, please contact +** Nokia at qt-info@nokia.com. +** +** +** +** +** +** +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QCOMPLETER_H +#define QCOMPLETER_H + +#include <QtCore/qobject.h> +#include <QtCore/qpoint.h> +#include <QtCore/qstring.h> +#include <QtCore/qabstractitemmodel.h> +#include <QtCore/qrect.h> + +QT_BEGIN_HEADER + +QT_BEGIN_NAMESPACE + +QT_MODULE(Gui) + +#ifndef QT_NO_COMPLETER + +class QCompleterPrivate; +class QAbstractItemView; +class QAbstractProxyModel; +class QWidget; + +class Q_GUI_EXPORT QCompleter : public QObject +{ + Q_OBJECT + Q_PROPERTY(QString completionPrefix READ completionPrefix WRITE setCompletionPrefix) + Q_PROPERTY(ModelSorting modelSorting READ modelSorting WRITE setModelSorting) + Q_PROPERTY(CompletionMode completionMode READ completionMode WRITE setCompletionMode) + Q_PROPERTY(int completionColumn READ completionColumn WRITE setCompletionColumn) + Q_PROPERTY(int completionRole READ completionRole WRITE setCompletionRole) + Q_PROPERTY(int maxVisibleItems READ maxVisibleItems WRITE setMaxVisibleItems) + Q_PROPERTY(Qt::CaseSensitivity caseSensitivity READ caseSensitivity WRITE setCaseSensitivity) + Q_PROPERTY(bool wrapAround READ wrapAround WRITE setWrapAround) + +public: + enum CompletionMode { + PopupCompletion, + UnfilteredPopupCompletion, + InlineCompletion + }; + + enum ModelSorting { + UnsortedModel = 0, + CaseSensitivelySortedModel, + CaseInsensitivelySortedModel + }; + + QCompleter(QObject *parent = 0); + QCompleter(QAbstractItemModel *model, QObject *parent = 0); +#ifndef QT_NO_STRINGLISTMODEL + QCompleter(const QStringList& completions, QObject *parent = 0); +#endif + ~QCompleter(); + + void setWidget(QWidget *widget); + QWidget *widget() const; + + void setModel(QAbstractItemModel *c); + QAbstractItemModel *model() const; + + void setCompletionMode(CompletionMode mode); + CompletionMode completionMode() const; + + QAbstractItemView *popup() const; + void setPopup(QAbstractItemView *popup); + + void setCaseSensitivity(Qt::CaseSensitivity caseSensitivity); + Qt::CaseSensitivity caseSensitivity() const; + + void setModelSorting(ModelSorting sorting); + ModelSorting modelSorting() const; + + void setCompletionColumn(int column); + int completionColumn() const; + + void setCompletionRole(int role); + int completionRole() const; + + bool wrapAround() const; + + int maxVisibleItems() const; + void setMaxVisibleItems(int maxItems); + + int completionCount() const; + bool setCurrentRow(int row); + int currentRow() const; + + QModelIndex currentIndex() const; + QString currentCompletion() const; + + QAbstractItemModel *completionModel() const; + + QString completionPrefix() const; + +public Q_SLOTS: + void setCompletionPrefix(const QString &prefix); + void complete(const QRect& rect = QRect()); + void setWrapAround(bool wrap); + +public: + virtual QString pathFromIndex(const QModelIndex &index) const; + virtual QStringList splitPath(const QString &path) const; + +protected: + bool eventFilter(QObject *o, QEvent *e); + bool event(QEvent *); + +Q_SIGNALS: + void activated(const QString &text); + void activated(const QModelIndex &index); + void highlighted(const QString &text); + void highlighted(const QModelIndex &index); + +private: + Q_DISABLE_COPY(QCompleter) + Q_DECLARE_PRIVATE(QCompleter) + + Q_PRIVATE_SLOT(d_func(), void _q_complete(QModelIndex)) + Q_PRIVATE_SLOT(d_func(), void _q_completionSelected(const QItemSelection&)) + Q_PRIVATE_SLOT(d_func(), void _q_autoResizePopup()) + Q_PRIVATE_SLOT(d_func(), void _q_fileSystemModelDirectoryLoaded(const QString&)) +}; + +#endif // QT_NO_COMPLETER + +QT_END_NAMESPACE + +QT_END_HEADER + +#endif // QCOMPLETER_H diff --git a/src/widgets/util/qcompleter_p.h b/src/widgets/util/qcompleter_p.h new file mode 100644 index 0000000000..83e7a27636 --- /dev/null +++ b/src/widgets/util/qcompleter_p.h @@ -0,0 +1,264 @@ +/**************************************************************************** +** +** Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies). +** All rights reserved. +** Contact: Nokia Corporation (qt-info@nokia.com) +** +** This file is part of the QtGui module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** No Commercial Usage +** This file contains pre-release code and may not be distributed. +** You may use this file in accordance with the terms and conditions +** contained in the Technology Preview License Agreement accompanying +** this package. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain additional +** rights. These rights are described in the Nokia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** If you have questions regarding the use of this file, please contact +** Nokia at qt-info@nokia.com. +** +** +** +** +** +** +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QCOMPLETER_P_H +#define QCOMPLETER_P_H + + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "private/qobject_p.h" + +#ifndef QT_NO_COMPLETER + +#include "QtGui/qtreeview.h" +#include "QtGui/qabstractproxymodel.h" +#include "qcompleter.h" +#include "QtGui/qitemdelegate.h" +#include "QtGui/qpainter.h" +#include "private/qabstractproxymodel_p.h" + +QT_BEGIN_NAMESPACE + +class QCompletionModel; + +class QCompleterPrivate : public QObjectPrivate +{ + Q_DECLARE_PUBLIC(QCompleter) + +public: + QCompleterPrivate(); + ~QCompleterPrivate() { delete popup; } + void init(QAbstractItemModel *model = 0); + + QPointer<QWidget> widget; + QCompletionModel *proxy; + QAbstractItemView *popup; + QCompleter::CompletionMode mode; + + QString prefix; + Qt::CaseSensitivity cs; + int role; + int column; + int maxVisibleItems; + QCompleter::ModelSorting sorting; + bool wrap; + + bool eatFocusOut; + QRect popupRect; + bool hiddenBecauseNoMatch; + + void showPopup(const QRect&); + void _q_complete(QModelIndex, bool = false); + void _q_completionSelected(const QItemSelection&); + void _q_autoResizePopup(); + void _q_fileSystemModelDirectoryLoaded(const QString &path); + void setCurrentIndex(QModelIndex, bool = true); +}; + +class QIndexMapper +{ +public: + QIndexMapper() : v(false), f(0), t(-1) { } + QIndexMapper(int f, int t) : v(false), f(f), t(t) { } + QIndexMapper(QVector<int> vec) : v(true), vector(vec), f(-1), t(-1) { } + + inline int count() const { return v ? vector.count() : t - f + 1; } + inline int operator[] (int index) const { return v ? vector[index] : f + index; } + inline int indexOf(int x) const { return v ? vector.indexOf(x) : ((t < f) ? -1 : x - f); } + inline bool isValid() const { return !isEmpty(); } + inline bool isEmpty() const { return v ? vector.isEmpty() : (t < f); } + inline void append(int x) { Q_ASSERT(v); vector.append(x); } + inline int first() const { return v ? vector.first() : f; } + inline int last() const { return v ? vector.last() : t; } + inline int from() const { Q_ASSERT(!v); return f; } + inline int to() const { Q_ASSERT(!v); return t; } + inline int cost() const { return vector.count()+2; } + +private: + bool v; + QVector<int> vector; + int f, t; +}; + +struct QMatchData { + QMatchData() : exactMatchIndex(-1) { } + QMatchData(const QIndexMapper& indices, int em, bool p) : + indices(indices), exactMatchIndex(em), partial(p) { } + QIndexMapper indices; + inline bool isValid() const { return indices.isValid(); } + int exactMatchIndex; + bool partial; +}; + +class QCompletionEngine +{ +public: + typedef QMap<QString, QMatchData> CacheItem; + typedef QMap<QModelIndex, CacheItem> Cache; + + QCompletionEngine(QCompleterPrivate *c) : c(c), curRow(-1), cost(0) { } + virtual ~QCompletionEngine() { } + + void filter(const QStringList &parts); + + QMatchData filterHistory(); + bool matchHint(QString, const QModelIndex&, QMatchData*); + + void saveInCache(QString, const QModelIndex&, const QMatchData&); + bool lookupCache(QString part, const QModelIndex& parent, QMatchData *m); + + virtual void filterOnDemand(int) { } + virtual QMatchData filter(const QString&, const QModelIndex&, int) = 0; + + int matchCount() const { return curMatch.indices.count() + historyMatch.indices.count(); } + + QMatchData curMatch, historyMatch; + QCompleterPrivate *c; + QStringList curParts; + QModelIndex curParent; + int curRow; + + Cache cache; + int cost; +}; + +class QSortedModelEngine : public QCompletionEngine +{ +public: + QSortedModelEngine(QCompleterPrivate *c) : QCompletionEngine(c) { } + QMatchData filter(const QString&, const QModelIndex&, int); + QIndexMapper indexHint(QString, const QModelIndex&, Qt::SortOrder); + Qt::SortOrder sortOrder(const QModelIndex&) const; +}; + +class QUnsortedModelEngine : public QCompletionEngine +{ +public: + QUnsortedModelEngine(QCompleterPrivate *c) : QCompletionEngine(c) { } + + void filterOnDemand(int); + QMatchData filter(const QString&, const QModelIndex&, int); +private: + int buildIndices(const QString& str, const QModelIndex& parent, int n, + const QIndexMapper& iv, QMatchData* m); +}; + +class QCompleterItemDelegate : public QItemDelegate +{ +public: + QCompleterItemDelegate(QAbstractItemView *view) + : QItemDelegate(view), view(view) { } + void paint(QPainter *p, const QStyleOptionViewItem& opt, const QModelIndex& idx) const { + QStyleOptionViewItem optCopy = opt; + optCopy.showDecorationSelected = true; + if (view->currentIndex() == idx) + optCopy.state |= QStyle::State_HasFocus; + QItemDelegate::paint(p, optCopy, idx); + } + +private: + QAbstractItemView *view; +}; + +class QCompletionModelPrivate; + +class QCompletionModel : public QAbstractProxyModel +{ + Q_OBJECT + +public: + QCompletionModel(QCompleterPrivate *c, QObject *parent); + + void createEngine(); + void setFiltered(bool); + void filter(const QStringList& parts); + int completionCount() const; + int currentRow() const { return engine->curRow; } + bool setCurrentRow(int row); + QModelIndex currentIndex(bool) const; + void resetModel(); + + QModelIndex index(int row, int column, const QModelIndex & = QModelIndex()) const; + int rowCount(const QModelIndex &index = QModelIndex()) const; + int columnCount(const QModelIndex &index = QModelIndex()) const; + bool hasChildren(const QModelIndex &parent = QModelIndex()) const; + QModelIndex parent(const QModelIndex & = QModelIndex()) const { return QModelIndex(); } + QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const; + + void setSourceModel(QAbstractItemModel *sourceModel); + QModelIndex mapToSource(const QModelIndex& proxyIndex) const; + QModelIndex mapFromSource(const QModelIndex& sourceIndex) const; + + QCompleterPrivate *c; + QScopedPointer<QCompletionEngine> engine; + bool showAll; + + Q_DECLARE_PRIVATE(QCompletionModel) + +signals: + void rowsAdded(); + +public Q_SLOTS: + void invalidate(); + void rowsInserted(); + void modelDestroyed(); +}; + +class QCompletionModelPrivate : public QAbstractProxyModelPrivate +{ + Q_DECLARE_PUBLIC(QCompletionModel) +}; + +QT_END_NAMESPACE + +#endif // QT_NO_COMPLETER + +#endif // QCOMPLETER_P_H diff --git a/src/widgets/util/qflickgesture.cpp b/src/widgets/util/qflickgesture.cpp new file mode 100644 index 0000000000..fdd2a95333 --- /dev/null +++ b/src/widgets/util/qflickgesture.cpp @@ -0,0 +1,715 @@ +/**************************************************************************** +** +** Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies). +** All rights reserved. +** Contact: Nokia Corporation (qt-info@nokia.com) +** +** This file is part of the QtGui module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** No Commercial Usage +** This file contains pre-release code and may not be distributed. +** You may use this file in accordance with the terms and conditions +** contained in the Technology Preview License Agreement accompanying +** this package. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain additional +** rights. These rights are described in the Nokia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** If you have questions regarding the use of this file, please contact +** Nokia at qt-info@nokia.com. +** +** +** +** +** +** +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qgesture.h" +#include "qapplication.h" +#include "qevent.h" +#include "qwidget.h" +#include "qgraphicsitem.h" +#include "qgraphicsscene.h" +#include "qgraphicssceneevent.h" +#include "qgraphicsview.h" +#include "qscroller.h" +#include "private/qevent_p.h" +#include "private/qflickgesture_p.h" +#include "qdebug.h" + +#ifndef QT_NO_GESTURES + +QT_BEGIN_NAMESPACE + +//#define QFLICKGESTURE_DEBUG + +#ifdef QFLICKGESTURE_DEBUG +# define qFGDebug qDebug +#else +# define qFGDebug while (false) qDebug +#endif + +extern bool qt_sendSpontaneousEvent(QObject *receiver, QEvent *event); + +static QMouseEvent *copyMouseEvent(QEvent *e) +{ + switch (e->type()) { + case QEvent::MouseButtonPress: + case QEvent::MouseButtonRelease: + case QEvent::MouseMove: { + QMouseEvent *me = static_cast<QMouseEvent *>(e); + return new QMouseEvent(me->type(), QPoint(0, 0), me->globalPos(), me->button(), me->buttons(), me->modifiers()); + } +#ifndef QT_NO_GRAPHICSVIEW + case QEvent::GraphicsSceneMousePress: + case QEvent::GraphicsSceneMouseRelease: + case QEvent::GraphicsSceneMouseMove: { + QGraphicsSceneMouseEvent *me = static_cast<QGraphicsSceneMouseEvent *>(e); +#if 1 + QEvent::Type met = me->type() == QEvent::GraphicsSceneMousePress ? QEvent::MouseButtonPress : + (me->type() == QEvent::GraphicsSceneMouseRelease ? QEvent::MouseButtonRelease : QEvent::MouseMove); + return new QMouseEvent(met, QPoint(0, 0), me->screenPos(), me->button(), me->buttons(), me->modifiers()); +#else + QGraphicsSceneMouseEvent *copy = new QGraphicsSceneMouseEvent(me->type()); + copy->setPos(me->pos()); + copy->setScenePos(me->scenePos()); + copy->setScreenPos(me->screenPos()); + for (int i = 0x1; i <= 0x10; i <<= 1) { + Qt::MouseButton button = Qt::MouseButton(i); + copy->setButtonDownPos(button, me->buttonDownPos(button)); + copy->setButtonDownScenePos(button, me->buttonDownScenePos(button)); + copy->setButtonDownScreenPos(button, me->buttonDownScreenPos(button)); + } + copy->setLastPos(me->lastPos()); + copy->setLastScenePos(me->lastScenePos()); + copy->setLastScreenPos(me->lastScreenPos()); + copy->setButtons(me->buttons()); + copy->setButton(me->button()); + copy->setModifiers(me->modifiers()); + return copy; +#endif + } +#endif // QT_NO_GRAPHICSVIEW + default: + return 0; + } +} + +class PressDelayHandler : public QObject +{ +private: + PressDelayHandler(QObject *parent = 0) + : QObject(parent) + , pressDelayTimer(0) + , sendingEvent(false) + , mouseButton(Qt::NoButton) + , mouseTarget(0) + { } + + static PressDelayHandler *inst; + +public: + enum { + UngrabMouseBefore = 1, + RegrabMouseAfterwards = 2 + }; + + static PressDelayHandler *instance() + { + static PressDelayHandler *inst = 0; + if (!inst) + inst = new PressDelayHandler(QCoreApplication::instance()); + return inst; + } + + bool shouldEventBeIgnored(QEvent *) const + { + return sendingEvent; + } + + bool isDelaying() const + { + return !pressDelayEvent.isNull(); + } + + void pressed(QEvent *e, int delay) + { + if (!pressDelayEvent) { + pressDelayEvent.reset(copyMouseEvent(e)); + pressDelayTimer = startTimer(delay); + mouseTarget = QApplication::widgetAt(pressDelayEvent->globalPos()); + mouseButton = pressDelayEvent->button(); + qFGDebug() << "QFG: consuming/delaying mouse press"; + } else { + qFGDebug() << "QFG: NOT consuming/delaying mouse press"; + } + e->setAccepted(true); + } + + bool released(QEvent *e, bool scrollerWasActive, bool scrollerIsActive) + { + // consume this event if the scroller was or is active + bool result = scrollerWasActive || scrollerIsActive; + + // stop the timer + if (pressDelayTimer) { + killTimer(pressDelayTimer); + pressDelayTimer = 0; + } + // we still haven't even sent the press, so do it now + if (pressDelayEvent && mouseTarget && !scrollerIsActive) { + QScopedPointer<QMouseEvent> releaseEvent(copyMouseEvent(e)); + + qFGDebug() << "QFG: re-sending mouse press (due to release) for " << mouseTarget; + sendMouseEvent(pressDelayEvent.data(), UngrabMouseBefore); + + qFGDebug() << "QFG: faking mouse release (due to release) for " << mouseTarget; + sendMouseEvent(releaseEvent.data()); + + result = true; // consume this event + } else if (mouseTarget && scrollerIsActive) { + // we grabbed the mouse expicitly when the scroller became active, so undo that now + sendMouseEvent(0, UngrabMouseBefore); + } + pressDelayEvent.reset(0); + mouseTarget = 0; + return result; + } + + void scrollerWasIntercepted() + { + qFGDebug() << "QFG: deleting delayed mouse press, since scroller was only intercepted"; + if (pressDelayEvent) { + // we still haven't even sent the press, so just throw it away now + if (pressDelayTimer) { + killTimer(pressDelayTimer); + pressDelayTimer = 0; + } + pressDelayEvent.reset(0); + } + mouseTarget = 0; + } + + void scrollerBecameActive() + { + if (pressDelayEvent) { + // we still haven't even sent the press, so just throw it away now + qFGDebug() << "QFG: deleting delayed mouse press, since scroller is active now"; + if (pressDelayTimer) { + killTimer(pressDelayTimer); + pressDelayTimer = 0; + } + pressDelayEvent.reset(0); + mouseTarget = 0; + } else if (mouseTarget) { + // we did send a press, so we need to fake a release now + Qt::MouseButtons mouseButtons = QApplication::mouseButtons(); + + // release all pressed mouse buttons + /*for (int i = 0; i < 32; ++i) { + if (mouseButtons & (1 << i)) { + Qt::MouseButton b = static_cast<Qt::MouseButton>(1 << i); + mouseButtons &= ~b; + QPoint farFarAway(-QWIDGETSIZE_MAX, -QWIDGETSIZE_MAX); + + qFGDebug() << "QFG: sending a fake mouse release at far-far-away to " << mouseTarget; + QMouseEvent re(QEvent::MouseButtonRelease, QPoint(), farFarAway, + b, mouseButtons, QApplication::keyboardModifiers()); + sendMouseEvent(&re); + } + }*/ + + QPoint farFarAway(-QWIDGETSIZE_MAX, -QWIDGETSIZE_MAX); + + qFGDebug() << "QFG: sending a fake mouse release at far-far-away to " << mouseTarget; + QMouseEvent re(QEvent::MouseButtonRelease, QPoint(), farFarAway, + mouseButton, QApplication::mouseButtons() & ~mouseButton, + QApplication::keyboardModifiers()); + sendMouseEvent(&re, RegrabMouseAfterwards); + // don't clear the mouseTarget just yet, since we need to explicitly ungrab the mouse on release! + } + } + +protected: + void timerEvent(QTimerEvent *e) + { + if (e->timerId() == pressDelayTimer) { + if (pressDelayEvent && mouseTarget) { + qFGDebug() << "QFG: timer event: re-sending mouse press to " << mouseTarget; + sendMouseEvent(pressDelayEvent.data(), UngrabMouseBefore); + } + pressDelayEvent.reset(0); + + if (pressDelayTimer) { + killTimer(pressDelayTimer); + pressDelayTimer = 0; + } + } + } + + void sendMouseEvent(QMouseEvent *me, int flags = 0) + { + if (mouseTarget) { + sendingEvent = true; + +#ifndef QT_NO_GRAPHICSVIEW + QGraphicsItem *grabber = 0; + if (mouseTarget->parentWidget()) { + if (QGraphicsView *gv = qobject_cast<QGraphicsView *>(mouseTarget->parentWidget())) { + if (gv->scene()) + grabber = gv->scene()->mouseGrabberItem(); + } + } + + if (grabber && (flags & UngrabMouseBefore)) { + // GraphicsView Mouse Handling Workaround #1: + // we need to ungrab the mouse before re-sending the press, + // since the scene had already set the mouse grabber to the + // original (and consumed) event's receiver + qFGDebug() << "QFG: ungrabbing" << grabber; + grabber->ungrabMouse(); + } +#endif // QT_NO_GRAPHICSVIEW + + if (me) { + QMouseEvent copy(me->type(), mouseTarget->mapFromGlobal(me->globalPos()), me->globalPos(), me->button(), me->buttons(), me->modifiers()); + qt_sendSpontaneousEvent(mouseTarget, ©); + } + +#ifndef QT_NO_GRAPHICSVIEW + if (grabber && (flags & RegrabMouseAfterwards)) { + // GraphicsView Mouse Handling Workaround #2: + // we need to re-grab the mouse after sending a faked mouse + // release, since we still need the mouse moves for the gesture + // (the scene will clear the item's mouse grabber status on + // release). + qFGDebug() << "QFG: re-grabbing" << grabber; + grabber->grabMouse(); + } +#endif + sendingEvent = false; + } + } + + +private: + int pressDelayTimer; + QScopedPointer<QMouseEvent> pressDelayEvent; + bool sendingEvent; + Qt::MouseButton mouseButton; + QPointer<QWidget> mouseTarget; +}; + + +/*! + \internal + \class QFlickGesture + \since 4.8 + \brief The QFlickGesture class describes a flicking gesture made by the user. + \ingroup gestures + The QFlickGesture is more complex than the QPanGesture that uses QScroller and QScrollerProperties + to decide if it is triggered. + This gesture is reacting on touch event as compared to the QMouseFlickGesture. + + \sa {Gestures Programming}, QScroller, QScrollerProperties, QMouseFlickGesture +*/ + +/*! + \internal +*/ +QFlickGesture::QFlickGesture(QObject *receiver, Qt::MouseButton button, QObject *parent) + : QGesture(*new QFlickGesturePrivate, parent) +{ + d_func()->q_ptr = this; + d_func()->receiver = receiver; + d_func()->receiverScroller = (receiver && QScroller::hasScroller(receiver)) ? QScroller::scroller(receiver) : 0; + d_func()->button = button; +} + +QFlickGesture::~QFlickGesture() +{ } + +QFlickGesturePrivate::QFlickGesturePrivate() + : receiverScroller(0), button(Qt::NoButton), macIgnoreWheel(false) +{ } + + +// +// QFlickGestureRecognizer +// + + +QFlickGestureRecognizer::QFlickGestureRecognizer(Qt::MouseButton button) +{ + this->button = button; +} + +/*! \reimp + */ +QGesture *QFlickGestureRecognizer::create(QObject *target) +{ +#ifndef QT_NO_GRAPHICSVIEW + QGraphicsObject *go = qobject_cast<QGraphicsObject*>(target); + if (go && button == Qt::NoButton) { + go->setAcceptTouchEvents(true); + } +#endif + return new QFlickGesture(target, button); +} + +/*! \internal + The recognize function detects a touch event suitable to start the attached QScroller. + The QFlickGesture will be triggered as soon as the scroller is no longer in the state + QScroller::Inactive or QScroller::Pressed. It will be finished or canceled + at the next QEvent::TouchEnd. + Note that the QScroller might continue scrolling (kinetically) at this point. + */ +QGestureRecognizer::Result QFlickGestureRecognizer::recognize(QGesture *state, + QObject *watched, + QEvent *event) +{ + Q_UNUSED(watched); + + static QElapsedTimer monotonicTimer; + if (!monotonicTimer.isValid()) + monotonicTimer.start(); + + QFlickGesture *q = static_cast<QFlickGesture *>(state); + QFlickGesturePrivate *d = q->d_func(); + + QScroller *scroller = d->receiverScroller; + if (!scroller) + return Ignore; // nothing to do without a scroller? + + QWidget *receiverWidget = qobject_cast<QWidget *>(d->receiver); +#ifndef QT_NO_GRAPHICSVIEW + QGraphicsObject *receiverGraphicsObject = qobject_cast<QGraphicsObject *>(d->receiver); +#endif + + // this is only set for events that we inject into the event loop via sendEvent() + if (PressDelayHandler::instance()->shouldEventBeIgnored(event)) { + //qFGDebug() << state << "QFG: ignored event: " << event->type(); + return Ignore; + } + + const QMouseEvent *me = 0; +#ifndef QT_NO_GRAPHICSVIEW + const QGraphicsSceneMouseEvent *gsme = 0; +#endif + const QTouchEvent *te = 0; + QPoint globalPos; + + // qFGDebug() << "FlickGesture "<<state<<"watched:"<<watched<<"receiver"<<d->receiver<<"event"<<event->type()<<"button"<<button; + + switch (event->type()) { + case QEvent::MouseButtonPress: + case QEvent::MouseButtonRelease: + case QEvent::MouseMove: + if (!receiverWidget) + return Ignore; + if (button != Qt::NoButton) { + me = static_cast<const QMouseEvent *>(event); + globalPos = me->globalPos(); + } + break; +#ifndef QT_NO_GRAPHICSVIEW + case QEvent::GraphicsSceneMousePress: + case QEvent::GraphicsSceneMouseRelease: + case QEvent::GraphicsSceneMouseMove: + if (!receiverGraphicsObject) + return Ignore; + if (button != Qt::NoButton) { + gsme = static_cast<const QGraphicsSceneMouseEvent *>(event); + globalPos = gsme->screenPos(); + } + break; +#endif + case QEvent::TouchBegin: + case QEvent::TouchEnd: + case QEvent::TouchUpdate: + if (button == Qt::NoButton) { + te = static_cast<const QTouchEvent *>(event); + if (!te->touchPoints().isEmpty()) + globalPos = te->touchPoints().at(0).screenPos().toPoint(); + } + break; + +#if defined(Q_WS_MAC) + // the only way to distinguish between real mouse wheels and wheel + // events generated by the native 2 finger swipe gesture is to listen + // for these events (according to Apple's Cocoa Event-Handling Guide) + + case QEvent::NativeGesture: { + QNativeGestureEvent *nge = static_cast<QNativeGestureEvent *>(event); + if (nge->gestureType == QNativeGestureEvent::GestureBegin) + d->macIgnoreWheel = true; + else if (nge->gestureType == QNativeGestureEvent::GestureEnd) + d->macIgnoreWheel = false; + break; + } +#endif + + // consume all wheel events if the scroller is active + case QEvent::Wheel: + if (d->macIgnoreWheel || (scroller->state() != QScroller::Inactive)) + return Ignore | ConsumeEventHint; + break; + + // consume all dbl click events if the scroller is active + case QEvent::MouseButtonDblClick: + if (scroller->state() != QScroller::Inactive) + return Ignore | ConsumeEventHint; + break; + + default: + break; + } + + if (!me +#ifndef QT_NO_GRAPHICSVIEW + && !gsme +#endif + && !te) // Neither mouse nor touch + return Ignore; + + // get the current pointer position in local coordinates. + QPointF point; + QScroller::Input inputType = (QScroller::Input) 0; + + switch (event->type()) { + case QEvent::MouseButtonPress: + if (me && me->button() == button && me->buttons() == button) { + point = me->globalPos(); + inputType = QScroller::InputPress; + } else if (me) { + scroller->stop(); + return CancelGesture; + } + break; + case QEvent::MouseButtonRelease: + if (me && me->button() == button) { + point = me->globalPos(); + inputType = QScroller::InputRelease; + } + break; + case QEvent::MouseMove: +#ifdef Q_OS_SYMBIAN + // Qt on Symbian tracks the button state internally, while Qt on Win/Mac/Unix + // relies on the windowing system to report the current buttons state. + if (me && (me->buttons() == button || !me->buttons())) { +#else + if (me && me->buttons() == button) { +#endif + point = me->globalPos(); + inputType = QScroller::InputMove; + } + break; + +#ifndef QT_NO_GRAPHICSVIEW + case QEvent::GraphicsSceneMousePress: + if (gsme && gsme->button() == button && gsme->buttons() == button) { + point = gsme->scenePos(); + inputType = QScroller::InputPress; + } else if (gsme) { + scroller->stop(); + return CancelGesture; + } + break; + case QEvent::GraphicsSceneMouseRelease: + if (gsme && gsme->button() == button) { + point = gsme->scenePos(); + inputType = QScroller::InputRelease; + } + break; + case QEvent::GraphicsSceneMouseMove: +#ifdef Q_OS_SYMBIAN + // Qt on Symbian tracks the button state internally, while Qt on Win/Mac/Unix + // relies on the windowing system to report the current buttons state. + if (gsme && (gsme->buttons() == button || !me->buttons())) { +#else + if (gsme && gsme->buttons() == button) { +#endif + point = gsme->scenePos(); + inputType = QScroller::InputMove; + } + break; +#endif + + case QEvent::TouchBegin: + inputType = QScroller::InputPress; + // fall through + case QEvent::TouchEnd: + if (!inputType) + inputType = QScroller::InputRelease; + // fallthrough + case QEvent::TouchUpdate: + if (!inputType) + inputType = QScroller::InputMove; + + if (te->deviceType() == QTouchEvent::TouchPad) { + if (te->touchPoints().count() != 2) // 2 fingers on pad + return Ignore; + + point = te->touchPoints().at(0).startScenePos() + + ((te->touchPoints().at(0).scenePos() - te->touchPoints().at(0).startScenePos()) + + (te->touchPoints().at(1).scenePos() - te->touchPoints().at(1).startScenePos())) / 2; + } else { // TouchScreen + if (te->touchPoints().count() != 1) // 1 finger on screen + return Ignore; + + point = te->touchPoints().at(0).scenePos(); + } + break; + + default: + break; + } + + // Check for an active scroller at globalPos + if (inputType == QScroller::InputPress) { + foreach (QScroller *as, QScroller::activeScrollers()) { + if (as != scroller) { + QRegion scrollerRegion; + + if (QWidget *w = qobject_cast<QWidget *>(as->target())) { + scrollerRegion = QRect(w->mapToGlobal(QPoint(0, 0)), w->size()); +#ifndef QT_NO_GRAPHICSVIEW + } else if (QGraphicsObject *go = qobject_cast<QGraphicsObject *>(as->target())) { + if (go->scene() && !go->scene()->views().isEmpty()) { + foreach (QGraphicsView *gv, go->scene()->views()) + scrollerRegion |= gv->mapFromScene(go->mapToScene(go->boundingRect())) + .translated(gv->mapToGlobal(QPoint(0, 0))); + } +#endif + } + // active scrollers always have priority + if (scrollerRegion.contains(globalPos)) + return Ignore; + } + } + } + + bool scrollerWasDragging = (scroller->state() == QScroller::Dragging); + bool scrollerWasScrolling = (scroller->state() == QScroller::Scrolling); + + if (inputType) { + if (QWidget *w = qobject_cast<QWidget *>(d->receiver)) + point = w->mapFromGlobal(point.toPoint()); +#ifndef QT_NO_GRAPHICSVIEW + else if (QGraphicsObject *go = qobject_cast<QGraphicsObject *>(d->receiver)) + point = go->mapFromScene(point); +#endif + + // inform the scroller about the new event + scroller->handleInput(inputType, point, monotonicTimer.elapsed()); + } + + // depending on the scroller state return the gesture state + Result result(0); + bool scrollerIsActive = (scroller->state() == QScroller::Dragging || + scroller->state() == QScroller::Scrolling); + + // Consume all mouse events while dragging or scrolling to avoid nasty + // side effects with Qt's standard widgets. + if ((me +#ifndef QT_NO_GRAPHICSVIEW + || gsme +#endif + ) && scrollerIsActive) + result |= ConsumeEventHint; + + // The only problem with this approach is that we consume the + // MouseRelease when we start the scrolling with a flick gesture, so we + // have to fake a MouseRelease "somewhere" to not mess with the internal + // states of Qt's widgets (a QPushButton would stay in 'pressed' state + // forever, if it doesn't receive a MouseRelease). + if (me +#ifndef QT_NO_GRAPHICSVIEW + || gsme +#endif + ) { + if (!scrollerWasDragging && !scrollerWasScrolling && scrollerIsActive) + PressDelayHandler::instance()->scrollerBecameActive(); + else if (scrollerWasScrolling && (scroller->state() == QScroller::Dragging || scroller->state() == QScroller::Inactive)) + PressDelayHandler::instance()->scrollerWasIntercepted(); + } + + if (!inputType) { + result |= Ignore; + } else { + switch (event->type()) { + case QEvent::MouseButtonPress: +#ifndef QT_NO_GRAPHICSVIEW + case QEvent::GraphicsSceneMousePress: +#endif + if (scroller->state() == QScroller::Pressed) { + int pressDelay = int(1000 * scroller->scrollerProperties().scrollMetric(QScrollerProperties::MousePressEventDelay).toReal()); + if (pressDelay > 0) { + result |= ConsumeEventHint; + + PressDelayHandler::instance()->pressed(event, pressDelay); + event->accept(); + } + } + // fall through + case QEvent::TouchBegin: + q->setHotSpot(globalPos); + result |= scrollerIsActive ? TriggerGesture : MayBeGesture; + break; + + case QEvent::MouseMove: +#ifndef QT_NO_GRAPHICSVIEW + case QEvent::GraphicsSceneMouseMove: +#endif + if (PressDelayHandler::instance()->isDelaying()) + result |= ConsumeEventHint; + // fall through + case QEvent::TouchUpdate: + result |= scrollerIsActive ? TriggerGesture : Ignore; + break; + +#ifndef QT_NO_GRAPHICSVIEW + case QEvent::GraphicsSceneMouseRelease: +#endif + case QEvent::MouseButtonRelease: + if (PressDelayHandler::instance()->released(event, scrollerWasDragging || scrollerWasScrolling, scrollerIsActive)) + result |= ConsumeEventHint; + // fall through + case QEvent::TouchEnd: + result |= scrollerIsActive ? FinishGesture : CancelGesture; + break; + + default: + result |= Ignore; + break; + } + } + return result; +} + + +/*! \reimp + */ +void QFlickGestureRecognizer::reset(QGesture *state) +{ + QGestureRecognizer::reset(state); +} + +QT_END_NAMESPACE + +#endif // QT_NO_GESTURES diff --git a/src/widgets/util/qflickgesture_p.h b/src/widgets/util/qflickgesture_p.h new file mode 100644 index 0000000000..451b579edd --- /dev/null +++ b/src/widgets/util/qflickgesture_p.h @@ -0,0 +1,113 @@ +/**************************************************************************** +** +** Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies). +** All rights reserved. +** Contact: Nokia Corporation (qt-info@nokia.com) +** +** This file is part of the QtGui module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** No Commercial Usage +** This file contains pre-release code and may not be distributed. +** You may use this file in accordance with the terms and conditions +** contained in the Technology Preview License Agreement accompanying +** this package. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain additional +** rights. These rights are described in the Nokia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** If you have questions regarding the use of this file, please contact +** Nokia at qt-info@nokia.com. +** +** +** +** +** +** +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QFLICKGESTURE_P_H +#define QFLICKGESTURE_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists for the convenience +// of other Qt classes. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "qevent.h" +#include "qgesturerecognizer.h" +#include "private/qgesture_p.h" +#include "qscroller.h" +#include "qscopedpointer.h" + +#ifndef QT_NO_GESTURES + +QT_BEGIN_NAMESPACE + +class QFlickGesturePrivate; +class QGraphicsItem; + +class Q_GUI_EXPORT QFlickGesture : public QGesture +{ + Q_OBJECT + Q_DECLARE_PRIVATE(QFlickGesture) + +public: + QFlickGesture(QObject *receiver, Qt::MouseButton button, QObject *parent = 0); + ~QFlickGesture(); + + friend class QFlickGestureRecognizer; +}; + +class PressDelayHandler; + +class QFlickGesturePrivate : public QGesturePrivate +{ + Q_DECLARE_PUBLIC(QFlickGesture) +public: + QFlickGesturePrivate(); + + QPointer<QObject> receiver; + QScroller *receiverScroller; + Qt::MouseButton button; // NoButton == Touch + bool macIgnoreWheel; + static PressDelayHandler *pressDelayHandler; +}; + +class QFlickGestureRecognizer : public QGestureRecognizer +{ +public: + QFlickGestureRecognizer(Qt::MouseButton button); + + QGesture *create(QObject *target); + QGestureRecognizer::Result recognize(QGesture *state, QObject *watched, QEvent *event); + void reset(QGesture *state); + +private: + Qt::MouseButton button; // NoButton == Touch +}; + +QT_END_NAMESPACE + +#endif // QT_NO_GESTURES + +#endif // QFLICKGESTURE_P_H diff --git a/src/widgets/util/qscroller.cpp b/src/widgets/util/qscroller.cpp new file mode 100644 index 0000000000..db128c136a --- /dev/null +++ b/src/widgets/util/qscroller.cpp @@ -0,0 +1,2059 @@ +/**************************************************************************** +** +** Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies). +** All rights reserved. +** Contact: Nokia Corporation (qt-info@nokia.com) +** +** This file is part of the QtGui module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** No Commercial Usage +** This file contains pre-release code and may not be distributed. +** You may use this file in accordance with the terms and conditions +** contained in the Technology Preview License Agreement accompanying +** this package. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain additional +** rights. These rights are described in the Nokia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** If you have questions regarding the use of this file, please contact +** Nokia at qt-info@nokia.com. +** +** +** +** +** +** +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qevent.h" +#include "qwidget.h" +#include "qscroller.h" +#include "private/qflickgesture_p.h" +#include "private/qscroller_p.h" +#include "qscrollerproperties.h" +#include "private/qscrollerproperties_p.h" +#include "qnumeric.h" +#include "math.h" + +#include <QTime> +#include <QElapsedTimer> +#include <QMap> +#include <QApplication> +#include <QAbstractScrollArea> +#include <QGraphicsObject> +#include <QGraphicsScene> +#include <QGraphicsView> +#include <QDesktopWidget> +#include <QtCore/qmath.h> +#include <QtGui/qevent.h> +#include <qnumeric.h> + +#include <QtDebug> + +#if defined(Q_WS_X11) +# include "private/qt_x11_p.h" +#endif + + +QT_BEGIN_NAMESPACE + +bool qt_sendSpontaneousEvent(QObject *receiver, QEvent *event); + +//#define QSCROLLER_DEBUG + +#ifdef QSCROLLER_DEBUG +# define qScrollerDebug qDebug +#else +# define qScrollerDebug while (false) qDebug +#endif + +QDebug &operator<<(QDebug &dbg, const QScrollerPrivate::ScrollSegment &s) +{ + dbg << "\n Time: start:" << s.startTime << " duration:" << s.deltaTime << " stop progress:" << s.stopProgress; + dbg << "\n Pos: start:" << s.startPos << " delta:" << s.deltaPos << " stop:" << s.stopPos; + dbg << "\n Curve: type:" << s.curve.type() << "\n"; + return dbg; +} + + +// a few helper operators to make the code below a lot more readable: +// otherwise a lot of ifs would have to be multi-line to check both the x +// and y coordinate separately. + +// returns true only if the abs. value of BOTH x and y are <= f +inline bool operator<=(const QPointF &p, qreal f) +{ + return (qAbs(p.x()) <= f) && (qAbs(p.y()) <= f); +} + +// returns true only if the abs. value of BOTH x and y are < f +inline bool operator<(const QPointF &p, qreal f) +{ + return (qAbs(p.x()) < f) && (qAbs(p.y()) < f); +} + +// returns true if the abs. value of EITHER x or y are >= f +inline bool operator>=(const QPointF &p, qreal f) +{ + return (qAbs(p.x()) >= f) || (qAbs(p.y()) >= f); +} + +// returns true if the abs. value of EITHER x or y are > f +inline bool operator>(const QPointF &p, qreal f) +{ + return (qAbs(p.x()) > f) || (qAbs(p.y()) > f); +} + +// returns a new point with both coordinates having the abs. value of the original one +inline QPointF qAbs(const QPointF &p) +{ + return QPointF(qAbs(p.x()), qAbs(p.y())); +} + +// returns a new point with all components of p1 multiplied by the corresponding components of p2 +inline QPointF operator*(const QPointF &p1, const QPointF &p2) +{ + return QPointF(p1.x() * p2.x(), p1.y() * p2.y()); +} + +// returns a new point with all components of p1 divided by the corresponding components of p2 +inline QPointF operator/(const QPointF &p1, const QPointF &p2) +{ + return QPointF(p1.x() / p2.x(), p1.y() / p2.y()); +} + +inline QPointF clampToRect(const QPointF &p, const QRectF &rect) +{ + qreal x = qBound(rect.left(), p.x(), rect.right()); + qreal y = qBound(rect.top(), p.y(), rect.bottom()); + return QPointF(x, y); +} + +// returns -1, 0 or +1 according to r being <0, ==0 or >0 +inline int qSign(qreal r) +{ + return (r < 0) ? -1 : ((r > 0) ? 1 : 0); +} + +// this version is not mathematically exact, but it just works for every +// easing curve type (even custom ones) + +static qreal differentialForProgress(const QEasingCurve &curve, qreal pos) +{ + const qreal dx = 0.01; + qreal left = (pos < qreal(0.5)) ? pos : pos - qreal(dx); + qreal right = (pos >= qreal(0.5)) ? pos : pos + qreal(dx); + qreal d = (curve.valueForProgress(right) - curve.valueForProgress(left)) / qreal(dx); + + //qScrollerDebug() << "differentialForProgress(type: " << curve.type() << ", pos: " << pos << ") = " << d; + + return d; +} + +// this version is not mathematically exact, but it just works for every +// easing curve type (even custom ones) + +static qreal progressForValue(const QEasingCurve &curve, qreal value) +{ + if (curve.type() >= QEasingCurve::InElastic && + curve.type() < QEasingCurve::Custom) { + qWarning("progressForValue(): QEasingCurves of type %d do not have an inverse, since they are not injective.", curve.type()); + return value; + } + if (value < qreal(0) || value > qreal(1)) + return value; + + qreal progress = value, left(0), right(1); + for (int iterations = 6; iterations; --iterations) { + qreal v = curve.valueForProgress(progress); + if (v < value) + left = progress; + else if (v > value) + right = progress; + else + break; + progress = (left + right) / qreal(2); + } + return progress; +} + + +#ifndef QT_NO_ANIMATION +class QScrollTimer : public QAbstractAnimation +{ +public: + QScrollTimer(QScrollerPrivate *_d) + : d(_d), ignoreUpdate(false), skip(0) + { } + + int duration() const + { + return -1; + } + + void start() + { + // QAbstractAnimation::start() will immediately call + // updateCurrentTime(), but our state is not set correctly yet + ignoreUpdate = true; + QAbstractAnimation::start(); + ignoreUpdate = false; + skip = 0; + } + +protected: + void updateCurrentTime(int /*currentTime*/) + { + if (!ignoreUpdate) { + if (++skip >= d->frameRateSkip()) { + skip = 0; + d->timerTick(); + } + } + } + +private: + QScrollerPrivate *d; + bool ignoreUpdate; + int skip; +}; +#endif // QT_NO_ANIMATION + +/*! + \class QScroller + \brief The QScroller class enables kinetic scrolling for any scrolling widget or graphics item. + \since 4.8 + + With kinetic scrolling, the user can push the widget in a given + direction and it will continue to scroll in this direction until it is + stopped either by the user or by friction. Aspects of inertia, friction + and other physical concepts can be changed in order to fine-tune an + intuitive user experience. + + The QScroller object is the object that stores the current position and + scrolling speed and takes care of updates. + QScroller can be triggered by a flick gesture + + \code + QWidget *w = ...; + QScroller::grabGesture(w, QScroller::LeftMouseButtonGesture); + \endcode + + or directly like this: + + \code + QWidget *w = ...; + QScroller *scroller = QScroller::scroller(w); + scroller->scrollTo(QPointF(100, 100)); + \endcode + + The scrolled QObjects receive a QScrollPrepareEvent whenever the scroller needs to + update its geometry information and a QScrollEvent whenever the content of the object should + actually be scrolled. + + The scroller uses the global QAbstractAnimation timer to generate its QScrollEvents. This + can be changed with QScrollerProperties::FrameRate on a per-QScroller basis. + + Several examples in the \c scroller examples directory show how QScroller, + QScrollEvent and the scroller gesture can be used. + + Even though this kinetic scroller has a large number of settings available via + QScrollerProperties, we recommend that you leave them all at their default, platform optimized + values. Before changing them you can experiment with the \c plot example in + the \c scroller examples directory. + + \sa QScrollEvent, QScrollPrepareEvent, QScrollerProperties +*/ + +typedef QMap<QObject *, QScroller *> ScrollerHash; +typedef QSet<QScroller *> ScrollerSet; + +Q_GLOBAL_STATIC(ScrollerHash, qt_allScrollers) +Q_GLOBAL_STATIC(ScrollerSet, qt_activeScrollers) + +/*! + Returns \c true if a QScroller object was already created for \a target; \c false otherwise. + + \sa scroller() +*/ +bool QScroller::hasScroller(QObject *target) +{ + return (qt_allScrollers()->value(target)); +} + +/*! + Returns the scroller for the given \a target. + As long as the object exists this function will always return the same QScroller instance. + If no QScroller exists for the \a target, one will implicitly be created. + At no point more than one QScroller will be active on an object. + + \sa hasScroller(), target() +*/ +QScroller *QScroller::scroller(QObject *target) +{ + if (!target) { + qWarning("QScroller::scroller() was called with a null target."); + return 0; + } + + if (qt_allScrollers()->contains(target)) + return qt_allScrollers()->value(target); + + QScroller *s = new QScroller(target); + qt_allScrollers()->insert(target, s); + return s; +} + +/*! + \overload + This is the const version of scroller(). +*/ +const QScroller *QScroller::scroller(const QObject *target) +{ + return scroller(const_cast<QObject*>(target)); +} + +/*! + Returns an application wide list of currently active QScroller objects. + Active QScroller objects are in a state() that is not QScroller::Inactive. + This function is useful when writing your own gesture recognizer. +*/ +QList<QScroller *> QScroller::activeScrollers() +{ + return qt_activeScrollers()->toList(); +} + +/*! + Returns the target object of this scroller. + \sa hasScroller(), scroller() + */ +QObject *QScroller::target() const +{ + Q_D(const QScroller); + return d->target; +} + +/*! + \fn QScroller::scrollerPropertiesChanged(const QScrollerProperties &newProperties); + + QScroller emits this signal whenever its scroller properties change. + \a newProperties are the new scroller properties. + + \sa scrollerProperties +*/ + + +/*! \property QScroller::scrollerProperties + \brief The scroller properties of this scroller. + The properties are used by the QScroller to determine its scrolling behavior. +*/ +QScrollerProperties QScroller::scrollerProperties() const +{ + Q_D(const QScroller); + return d->properties; +} + +void QScroller::setScrollerProperties(const QScrollerProperties &sp) +{ + Q_D(QScroller); + if (d->properties != sp) { + d->properties = sp; + emit scrollerPropertiesChanged(sp); + + // we need to force the recalculation here, since the overshootPolicy may have changed and + // existing segments may include an overshoot animation. + d->recalcScrollingSegments(true); + } +} + +#ifndef QT_NO_GESTURES + +/*! + Registers a custom scroll gesture recognizer, grabs it for the \a + target and returns the resulting gesture type. If \a scrollGestureType is + set to TouchGesture the gesture triggers on touch events. If it is set to + one of LeftMouseButtonGesture, RightMouseButtonGesture or + MiddleMouseButtonGesture it triggers on mouse events of the + corresponding button. + + Only one scroll gesture can be active on a single object at the same + time. If you call this function twice on the same object, it will + ungrab the existing gesture before grabbing the new one. + + \note To avoid unwanted side-effects, mouse events are consumed while + the gesture is triggered. Since the initial mouse press event is + not consumed, the gesture sends a fake mouse release event + at the global position \c{(INT_MIN, INT_MIN)}. This ensures that + internal states of the widget that received the original mouse press + are consistent. + + \sa ungrabGesture, grabbedGesture +*/ +Qt::GestureType QScroller::grabGesture(QObject *target, ScrollerGestureType scrollGestureType) +{ + // ensure that a scroller for target is created + QScroller *s = scroller(target); + if (!s) + return Qt::GestureType(0); + + QScrollerPrivate *sp = s->d_ptr; + if (sp->recognizer) + ungrabGesture(target); // ungrab the old gesture + + Qt::MouseButton button; + switch (scrollGestureType) { + case LeftMouseButtonGesture : button = Qt::LeftButton; break; + case RightMouseButtonGesture : button = Qt::RightButton; break; + case MiddleMouseButtonGesture: button = Qt::MiddleButton; break; + default : + case TouchGesture : button = Qt::NoButton; break; // NoButton == Touch + } + + sp->recognizer = new QFlickGestureRecognizer(button); + sp->recognizerType = QGestureRecognizer::registerRecognizer(sp->recognizer); + + if (target->isWidgetType()) { + QWidget *widget = static_cast<QWidget *>(target); + widget->grabGesture(sp->recognizerType); + if (scrollGestureType == TouchGesture) + widget->setAttribute(Qt::WA_AcceptTouchEvents); +#ifndef QT_NO_GRAPHICSVIEW + } else if (QGraphicsObject *go = qobject_cast<QGraphicsObject*>(target)) { + if (scrollGestureType == TouchGesture) + go->setAcceptTouchEvents(true); + go->grabGesture(sp->recognizerType); +#endif // QT_NO_GRAPHICSVIEW + } + return sp->recognizerType; +} + +/*! + Returns the gesture type currently grabbed for the \a target or 0 if no + gesture is grabbed. + + \sa grabGesture, ungrabGesture +*/ +Qt::GestureType QScroller::grabbedGesture(QObject *target) +{ + QScroller *s = scroller(target); + if (s && s->d_ptr) + return s->d_ptr->recognizerType; + else + return Qt::GestureType(0); +} + +/*! + Ungrabs the gesture for the \a target. + Does nothing if no gesture is grabbed. + + \sa grabGesture, grabbedGesture +*/ +void QScroller::ungrabGesture(QObject *target) +{ + QScroller *s = scroller(target); + if (!s) + return; + + QScrollerPrivate *sp = s->d_ptr; + if (!sp->recognizer) + return; // nothing to do + + if (target->isWidgetType()) { + QWidget *widget = static_cast<QWidget *>(target); + widget->ungrabGesture(sp->recognizerType); +#ifndef QT_NO_GRAPHICSVIEW + } else if (QGraphicsObject *go = qobject_cast<QGraphicsObject*>(target)) { + go->ungrabGesture(sp->recognizerType); +#endif + } + + QGestureRecognizer::unregisterRecognizer(sp->recognizerType); + // do not delete the recognizer. The QGestureManager is doing this. + sp->recognizer = 0; +} + +#endif // QT_NO_GESTURES + +/*! + \internal +*/ +QScroller::QScroller(QObject *target) + : d_ptr(new QScrollerPrivate(this, target)) +{ + Q_ASSERT(target); // you can't create a scroller without a target in any normal way + Q_D(QScroller); + d->init(); +} + +/*! + \internal +*/ +QScroller::~QScroller() +{ + Q_D(QScroller); +#ifndef QT_NO_GESTURES + QGestureRecognizer::unregisterRecognizer(d->recognizerType); + // do not delete the recognizer. The QGestureManager is doing this. + d->recognizer = 0; +#endif + qt_allScrollers()->remove(d->target); + qt_activeScrollers()->remove(this); + + delete d_ptr; +} + + +/*! + \fn QScroller::stateChanged(QScroller::State newState); + + QScroller emits this signal whenever the state changes. \a newState is the new State. + + \sa state +*/ + +/*! + \property QScroller::state + \brief the state of the scroller + + \sa QScroller::State +*/ +QScroller::State QScroller::state() const +{ + Q_D(const QScroller); + return d->state; +} + +/*! + Stops the scroller and resets its state back to Inactive. +*/ +void QScroller::stop() +{ + Q_D(QScroller); + if (d->state != Inactive) { + QPointF here = clampToRect(d->contentPosition, d->contentPosRange); + qreal snapX = d->nextSnapPos(here.x(), 0, Qt::Horizontal); + qreal snapY = d->nextSnapPos(here.y(), 0, Qt::Vertical); + QPointF snap = here; + if (!qIsNaN(snapX)) + snap.setX(snapX); + if (!qIsNaN(snapY)) + snap.setY(snapY); + d->contentPosition = snap; + d->overshootPosition = QPointF(0, 0); + + d->setState(Inactive); + } +} + +/*! + Returns the pixel per meter metric for the scrolled widget. + + The value is reported for both the x and y axis separately by using a QPointF. + + \note Please note that this value should be physically correct. The actual DPI settings + that Qt returns for the display may be reported wrongly on purpose by the underlying + windowing system, for example on Mac OS X or Maemo 5. +*/ +QPointF QScroller::pixelPerMeter() const +{ + Q_D(const QScroller); + QPointF ppm = d->pixelPerMeter; + +#ifndef QT_NO_GRAPHICSVIEW + if (QGraphicsObject *go = qobject_cast<QGraphicsObject *>(d->target)) { + QTransform viewtr; + //TODO: the first view isn't really correct - maybe use an additional field in the prepare event? + if (go->scene() && !go->scene()->views().isEmpty()) + viewtr = go->scene()->views().first()->viewportTransform(); + QTransform tr = go->deviceTransform(viewtr); + if (tr.isScaling()) { + QPointF p0 = tr.map(QPointF(0, 0)); + QPointF px = tr.map(QPointF(1, 0)); + QPointF py = tr.map(QPointF(0, 1)); + ppm.rx() /= QLineF(p0, px).length(); + ppm.ry() /= QLineF(p0, py).length(); + } + } +#endif // QT_NO_GRAPHICSVIEW + return ppm; +} + +/*! + Returns the current scrolling velocity in meter per second when the state is Scrolling or Dragging. + Returns a zero velocity otherwise. + + The velocity is reported for both the x and y axis separately by using a QPointF. + + \sa pixelPerMeter() +*/ +QPointF QScroller::velocity() const +{ + Q_D(const QScroller); + const QScrollerPropertiesPrivate *sp = d->properties.d.data(); + + switch (state()) { + case Dragging: + return d->releaseVelocity; + case Scrolling: { + QPointF vel; + qint64 now = d->monotonicTimer.elapsed(); + + if (!d->xSegments.isEmpty()) { + const QScrollerPrivate::ScrollSegment &s = d->xSegments.head(); + qreal progress = qreal(now - s.startTime) / qreal(s.deltaTime); + qreal v = qSign(s.deltaPos) * qreal(s.deltaTime) / qreal(1000) * sp->decelerationFactor * qreal(0.5) * differentialForProgress(s.curve, progress); + vel.setX(v); + } + + if (!d->ySegments.isEmpty()) { + const QScrollerPrivate::ScrollSegment &s = d->ySegments.head(); + qreal progress = qreal(now - s.startTime) / qreal(s.deltaTime); + qreal v = qSign(s.deltaPos) * qreal(s.deltaTime) / qreal(1000) * sp->decelerationFactor * qreal(0.5) * differentialForProgress(s.curve, progress); + vel.setY(v); + } + return vel; + } + default: + return QPointF(0, 0); + } +} + +/*! + Returns the estimated final position for the current scroll movement. + Returns the current position if the scroller state is not Scrolling. + The result is undefined when the scroller state is Inactive. + + The target position is in pixel. + + \sa pixelPerMeter(), scrollTo() +*/ +QPointF QScroller::finalPosition() const +{ + Q_D(const QScroller); + return QPointF(d->scrollingSegmentsEndPos(Qt::Horizontal), + d->scrollingSegmentsEndPos(Qt::Vertical)); +} + +/*! + Starts scrolling the widget so that point \a pos is at the top-left position in + the viewport. + + The behaviour when scrolling outside the valid scroll area is undefined. + In this case the scroller might or might not overshoot. + + The scrolling speed will be calculated so that the given position will + be reached after a platform-defined time span. + + \a pos is given in viewport coordinates. + + \sa ensureVisible() +*/ +void QScroller::scrollTo(const QPointF &pos) +{ + // we could make this adjustable via QScrollerProperties + scrollTo(pos, 300); +} + +/*! \overload + + This version will reach its destination position in \a scrollTime milliseconds. +*/ +void QScroller::scrollTo(const QPointF &pos, int scrollTime) +{ + Q_D(QScroller); + + if (d->state == Pressed || d->state == Dragging ) + return; + + // no need to resend a prepare event if we are already scrolling + if (d->state == Inactive && !d->prepareScrolling(QPointF())) + return; + + QPointF newpos = clampToRect(pos, d->contentPosRange); + qreal snapX = d->nextSnapPos(newpos.x(), 0, Qt::Horizontal); + qreal snapY = d->nextSnapPos(newpos.y(), 0, Qt::Vertical); + if (!qIsNaN(snapX)) + newpos.setX(snapX); + if (!qIsNaN(snapY)) + newpos.setY(snapY); + + qScrollerDebug() << "QScroller::scrollTo(req:" << pos << " [pix] / snap:" << newpos << ", " << scrollTime << " [ms])"; + + if (newpos == d->contentPosition + d->overshootPosition) + return; + + QPointF vel = velocity(); + + if (scrollTime < 0) + scrollTime = 0; + qreal time = qreal(scrollTime) / 1000; + + d->createScrollToSegments(vel.x(), time, newpos.x(), Qt::Horizontal, QScrollerPrivate::ScrollTypeScrollTo); + d->createScrollToSegments(vel.y(), time, newpos.y(), Qt::Vertical, QScrollerPrivate::ScrollTypeScrollTo); + + if (!scrollTime) + d->setContentPositionHelperScrolling(); + d->setState(scrollTime ? Scrolling : Inactive); +} + +/*! + Starts scrolling so that the rectangle \a rect is visible inside the + viewport with additional margins specified in pixels by \a xmargin and \a ymargin around + the rect. + + In cases where it is not possible to fit the rect plus margins inside the viewport the contents + are scrolled so that as much as possible is visible from \a rect. + + The scrolling speed is calculated so that the given position is reached after a platform-defined + time span. + + This function performs the actual scrolling by calling scrollTo(). + + \sa scrollTo +*/ +void QScroller::ensureVisible(const QRectF &rect, qreal xmargin, qreal ymargin) +{ + // we could make this adjustable via QScrollerProperties + ensureVisible(rect, xmargin, ymargin, 1000); +} + +/*! \overload + + This version will reach its destination position in \a scrollTime milliseconds. +*/ +void QScroller::ensureVisible(const QRectF &rect, qreal xmargin, qreal ymargin, int scrollTime) +{ + Q_D(QScroller); + + if (d->state == Pressed || d->state == Dragging ) + return; + + if (d->state == Inactive && !d->prepareScrolling(QPointF())) + return; + + // -- calculate the current pos (or the position after the current scroll) + QPointF startPos = d->contentPosition + d->overshootPosition; + startPos = QPointF(d->scrollingSegmentsEndPos(Qt::Horizontal), + d->scrollingSegmentsEndPos(Qt::Vertical)); + + QRectF marginRect(rect.x() - xmargin, rect.y() - ymargin, + rect.width() + 2 * xmargin, rect.height() + 2 * ymargin); + + QSizeF visible = d->viewportSize; + QRectF visibleRect(startPos, visible); + + qScrollerDebug() << "QScroller::ensureVisible(" << rect << " [pix], " << xmargin << " [pix], " << ymargin << " [pix], " << scrollTime << "[ms])"; + qScrollerDebug() << " --> content position:" << d->contentPosition; + + if (visibleRect.contains(marginRect)) + return; + + QPointF newPos = startPos; + + if (visibleRect.width() < rect.width()) { + // at least try to move the rect into view + if (rect.left() > visibleRect.left()) + newPos.setX(rect.left()); + else if (rect.right() < visibleRect.right()) + newPos.setX(rect.right() - visible.width()); + + } else if (visibleRect.width() < marginRect.width()) { + newPos.setX(rect.center().x() - visibleRect.width() / 2); + } else if (marginRect.left() > visibleRect.left()) { + newPos.setX(marginRect.left()); + } else if (marginRect.right() < visibleRect.right()) { + newPos.setX(marginRect.right() - visible.width()); + } + + if (visibleRect.height() < rect.height()) { + // at least try to move the rect into view + if (rect.top() > visibleRect.top()) + newPos.setX(rect.top()); + else if (rect.bottom() < visibleRect.bottom()) + newPos.setX(rect.bottom() - visible.height()); + + } else if (visibleRect.height() < marginRect.height()) { + newPos.setY(rect.center().y() - visibleRect.height() / 2); + } else if (marginRect.top() > visibleRect.top()) { + newPos.setY(marginRect.top()); + } else if (marginRect.bottom() < visibleRect.bottom()) { + newPos.setY(marginRect.bottom() - visible.height()); + } + + // clamp to maximum content position + newPos = clampToRect(newPos, d->contentPosRange); + if (newPos == startPos) + return; + + scrollTo(newPos, scrollTime); +} + +/*! This function resends the QScrollPrepareEvent. + Calling resendPrepareEvent triggers a QScrollPrepareEvent from the scroller. + This allows the receiver to re-set content position and content size while + scrolling. + Calling this function while in the Inactive state is useless as the prepare event + is sent again before scrolling starts. + */ +void QScroller::resendPrepareEvent() +{ + Q_D(QScroller); + d->prepareScrolling(d->pressPosition); +} + +/*! Set the snap positions for the horizontal axis to a list of \a positions. + This overwrites all previously set snap positions and also a previously + set snapping interval. + Snapping can be deactivated by setting an empty list of positions. + */ +void QScroller::setSnapPositionsX(const QList<qreal> &positions) +{ + Q_D(QScroller); + d->snapPositionsX = positions; + d->snapIntervalX = 0.0; + + d->recalcScrollingSegments(); +} + +/*! Set the snap positions for the horizontal axis to regular spaced intervals. + The first snap position is at \a first. The next at \a first + \a interval. + This can be used to implement a list header. + This overwrites all previously set snap positions and also a previously + set snapping interval. + Snapping can be deactivated by setting an interval of 0.0 + */ +void QScroller::setSnapPositionsX(qreal first, qreal interval) +{ + Q_D(QScroller); + d->snapFirstX = first; + d->snapIntervalX = interval; + d->snapPositionsX.clear(); + + d->recalcScrollingSegments(); +} + +/*! Set the snap positions for the vertical axis to a list of \a positions. + This overwrites all previously set snap positions and also a previously + set snapping interval. + Snapping can be deactivated by setting an empty list of positions. + */ +void QScroller::setSnapPositionsY(const QList<qreal> &positions) +{ + Q_D(QScroller); + d->snapPositionsY = positions; + d->snapIntervalY = 0.0; + + d->recalcScrollingSegments(); +} + +/*! Set the snap positions for the vertical axis to regular spaced intervals. + The first snap position is at \a first. The next at \a first + \a interval. + This overwrites all previously set snap positions and also a previously + set snapping interval. + Snapping can be deactivated by setting an interval of 0.0 + */ +void QScroller::setSnapPositionsY(qreal first, qreal interval) +{ + Q_D(QScroller); + d->snapFirstY = first; + d->snapIntervalY = interval; + d->snapPositionsY.clear(); + + d->recalcScrollingSegments(); +} + + + +// -------------- private ------------ + +QScrollerPrivate::QScrollerPrivate(QScroller *q, QObject *_target) + : target(_target) +#ifndef QT_NO_GESTURES + , recognizer(0) + , recognizerType(Qt::CustomGesture) +#endif + , state(QScroller::Inactive) + , firstScroll(true) + , pressTimestamp(0) + , lastTimestamp(0) + , snapFirstX(-1.0) + , snapIntervalX(0.0) + , snapFirstY(-1.0) + , snapIntervalY(0.0) +#ifndef QT_NO_ANIMATION + , scrollTimer(new QScrollTimer(this)) +#endif + , q_ptr(q) +{ + connect(target, SIGNAL(destroyed(QObject*)), this, SLOT(targetDestroyed())); +} + +void QScrollerPrivate::init() +{ + setDpiFromWidget(0); + monotonicTimer.start(); +} + +void QScrollerPrivate::sendEvent(QObject *o, QEvent *e) +{ + qt_sendSpontaneousEvent(o, e); +} + +const char *QScrollerPrivate::stateName(QScroller::State state) +{ + switch (state) { + case QScroller::Inactive: return "inactive"; + case QScroller::Pressed: return "pressed"; + case QScroller::Dragging: return "dragging"; + case QScroller::Scrolling: return "scrolling"; + default: return "(invalid)"; + } +} + +const char *QScrollerPrivate::inputName(QScroller::Input input) +{ + switch (input) { + case QScroller::InputPress: return "press"; + case QScroller::InputMove: return "move"; + case QScroller::InputRelease: return "release"; + default: return "(invalid)"; + } +} + +void QScrollerPrivate::targetDestroyed() +{ +#ifndef QT_NO_ANIMATION + scrollTimer->stop(); +#endif + delete q_ptr; +} + +void QScrollerPrivate::timerTick() +{ + struct timerevent { + QScroller::State state; + typedef void (QScrollerPrivate::*timerhandler_t)(); + timerhandler_t handler; + }; + + timerevent timerevents[] = { + { QScroller::Dragging, &QScrollerPrivate::timerEventWhileDragging }, + { QScroller::Scrolling, &QScrollerPrivate::timerEventWhileScrolling }, + }; + + for (int i = 0; i < int(sizeof(timerevents) / sizeof(*timerevents)); ++i) { + timerevent *te = timerevents + i; + + if (state == te->state) { + (this->*te->handler)(); + return; + } + } + +#ifndef QT_NO_ANIMATION + scrollTimer->stop(); +#endif +} + +/*! + This function is used by gesture recognizers to inform the scroller about a new input event. + The scroller changes its internal state() according to the input event and its attached + scroller properties. The scroller doesn't distinguish between the kind of input device the + event came from. Therefore the event needs to be split into the \a input type, a \a position and a + milli-second \a timestamp. The \a position needs to be in the target's coordinate system. + + The return value is \c true if the event should be consumed by the calling filter or \c false + if the event should be forwarded to the control. + + \note Using grabGesture() should be sufficient for most use cases. +*/ +bool QScroller::handleInput(Input input, const QPointF &position, qint64 timestamp) +{ + Q_D(QScroller); + + qScrollerDebug() << "QScroller::handleInput(" << input << ", " << d->stateName(d->state) << ", " << position << ", " << timestamp << ")"; + struct statechange { + State state; + Input input; + typedef bool (QScrollerPrivate::*inputhandler_t)(const QPointF &position, qint64 timestamp); + inputhandler_t handler; + }; + + statechange statechanges[] = { + { QScroller::Inactive, InputPress, &QScrollerPrivate::pressWhileInactive }, + { QScroller::Pressed, InputMove, &QScrollerPrivate::moveWhilePressed }, + { QScroller::Pressed, InputRelease, &QScrollerPrivate::releaseWhilePressed }, + { QScroller::Dragging, InputMove, &QScrollerPrivate::moveWhileDragging }, + { QScroller::Dragging, InputRelease, &QScrollerPrivate::releaseWhileDragging }, + { QScroller::Scrolling, InputPress, &QScrollerPrivate::pressWhileScrolling } + }; + + for (int i = 0; i < int(sizeof(statechanges) / sizeof(*statechanges)); ++i) { + statechange *sc = statechanges + i; + + if (d->state == sc->state && input == sc->input) + return (d->*sc->handler)(position - d->overshootPosition, timestamp); + } + return false; +} + +#if !defined(Q_WS_MAC) +// the Mac version is implemented in qscroller_mac.mm + +QPointF QScrollerPrivate::realDpi(int screen) +{ +# ifdef Q_WS_MAEMO_5 + Q_UNUSED(screen); + + // The DPI value is hardcoded to 96 on Maemo5: + // https://projects.maemo.org/bugzilla/show_bug.cgi?id=152525 + // This value (260) is only correct for the N900 though, but + // there's no way to get the real DPI at run time. + return QPointF(260, 260); + +# elif defined(Q_WS_X11) && !defined(QT_NO_XRANDR) + if (X11 && X11->use_xrandr && X11->ptrXRRSizes && X11->ptrXRRRootToScreen) { + int nsizes = 0; + // QDesktopWidget is based on Xinerama screens, which do not always + // correspond to RandR screens: NVidia's TwinView e.g. will show up + // as 2 screens in QDesktopWidget, but libXRandR will only see 1 screen. + // (although with the combined size of the Xinerama screens). + // Additionally, libXrandr will simply crash when calling XRRSizes + // for (the non-existent) screen 1 in this scenario. + Window root = RootWindow(X11->display, screen == -1 ? X11->defaultScreen : screen); + int randrscreen = (root != XNone) ? X11->ptrXRRRootToScreen(X11->display, root) : -1; + + XRRScreenSize *sizes = X11->ptrXRRSizes(X11->display, randrscreen == -1 ? 0 : randrscreen, &nsizes); + if (nsizes > 0 && sizes && sizes->width && sizes->height && sizes->mwidth && sizes->mheight) { + qScrollerDebug() << "XRandR DPI:" << QPointF(qreal(25.4) * qreal(sizes->width) / qreal(sizes->mwidth), + qreal(25.4) * qreal(sizes->height) / qreal(sizes->mheight)); + return QPointF(qreal(25.4) * qreal(sizes->width) / qreal(sizes->mwidth), + qreal(25.4) * qreal(sizes->height) / qreal(sizes->mheight)); + } + } +# endif + + QWidget *w = QApplication::desktop()->screen(screen); + return QPointF(w->physicalDpiX(), w->physicalDpiY()); +} + +#endif // !Q_WS_MAC + + +/*! \internal + Returns the resolution of the used screen. +*/ +QPointF QScrollerPrivate::dpi() const +{ + return pixelPerMeter * qreal(0.0254); +} + +/*! \internal + Sets the resolution used for scrolling. + This resolution is only used by the kinetic scroller. If you change this + then the scroller will behave quite different as a lot of the values are + given in physical distances (millimeter). +*/ +void QScrollerPrivate::setDpi(const QPointF &dpi) +{ + pixelPerMeter = dpi / qreal(0.0254); +} + +/*! \internal + Sets the dpi used for scrolling to the value of the widget. +*/ +void QScrollerPrivate::setDpiFromWidget(QWidget *widget) +{ + QDesktopWidget *dw = QApplication::desktop(); + setDpi(realDpi(widget ? dw->screenNumber(widget) : dw->primaryScreen())); +} + +/*! \internal + Updates the velocity during dragging. + Sets releaseVelocity. +*/ +void QScrollerPrivate::updateVelocity(const QPointF &deltaPixelRaw, qint64 deltaTime) +{ + if (deltaTime <= 0) + return; + + Q_Q(QScroller); + QPointF ppm = q->pixelPerMeter(); + const QScrollerPropertiesPrivate *sp = properties.d.data(); + QPointF deltaPixel = deltaPixelRaw; + + qScrollerDebug() << "QScroller::updateVelocity(" << deltaPixelRaw << " [delta pix], " << deltaTime << " [delta ms])"; + + // faster than 2.5mm/ms seems bogus (that would be a screen height in ~20 ms) + if (((deltaPixelRaw / qreal(deltaTime)).manhattanLength() / ((ppm.x() + ppm.y()) / 2) * 1000) > qreal(2.5)) + deltaPixel = deltaPixelRaw * qreal(2.5) * ppm / 1000 / (deltaPixelRaw / qreal(deltaTime)).manhattanLength(); + + QPointF newv = -deltaPixel / qreal(deltaTime) * qreal(1000) / ppm; + // around 95% of all updates are in the [1..50] ms range, so make sure + // to scale the smoothing factor over that range: this way a 50ms update + // will have full impact, while 5ms update will only have a 10% impact. + qreal smoothing = sp->dragVelocitySmoothingFactor * qMin(qreal(deltaTime), qreal(50)) / qreal(50); + + // only smooth if we already have a release velocity and only if the + // user hasn't stopped to move his finger for more than 100ms + if ((releaseVelocity != QPointF(0, 0)) && (deltaTime < 100)) { + qScrollerDebug() << "SMOOTHED from " << newv << " to " << newv * smoothing + releaseVelocity * (qreal(1) - smoothing); + // smooth x or y only if the new velocity is either 0 or at least in + // the same direction of the release velocity + if (!newv.x() || (qSign(releaseVelocity.x()) == qSign(newv.x()))) + newv.setX(newv.x() * smoothing + releaseVelocity.x() * (qreal(1) - smoothing)); + if (!newv.y() || (qSign(releaseVelocity.y()) == qSign(newv.y()))) + newv.setY(newv.y() * smoothing + releaseVelocity.y() * (qreal(1) - smoothing)); + } else + qScrollerDebug() << "NO SMOOTHING to " << newv; + + releaseVelocity.setX(qBound(-sp->maximumVelocity, newv.x(), sp->maximumVelocity)); + releaseVelocity.setY(qBound(-sp->maximumVelocity, newv.y(), sp->maximumVelocity)); + + qScrollerDebug() << " --> new velocity:" << releaseVelocity; +} + +void QScrollerPrivate::pushSegment(ScrollType type, qreal deltaTime, qreal stopProgress, qreal startPos, qreal deltaPos, qreal stopPos, QEasingCurve::Type curve, Qt::Orientation orientation) +{ + if (startPos == stopPos || deltaPos == 0) + return; + + ScrollSegment s; + if (orientation == Qt::Horizontal && !xSegments.isEmpty()) + s.startTime = xSegments.last().startTime + xSegments.last().deltaTime * xSegments.last().stopProgress; + else if (orientation == Qt::Vertical && !ySegments.isEmpty()) + s.startTime = ySegments.last().startTime + ySegments.last().deltaTime * ySegments.last().stopProgress; + else + s.startTime = monotonicTimer.elapsed(); + + s.startPos = startPos; + s.deltaPos = deltaPos; + s.stopPos = stopPos; + s.deltaTime = deltaTime * 1000; + s.stopProgress = stopProgress; + s.curve.setType(curve); + s.type = type; + + if (orientation == Qt::Horizontal) + xSegments.enqueue(s); + else + ySegments.enqueue(s); + + qScrollerDebug() << "+++ Added a new ScrollSegment: " << s; +} + + +/*! \internal + Clears the old segments and recalculates them if the current segments are not longer valid +*/ +void QScrollerPrivate::recalcScrollingSegments(bool forceRecalc) +{ + Q_Q(QScroller); + QPointF ppm = q->pixelPerMeter(); + + releaseVelocity = q->velocity(); + + if (forceRecalc || !scrollingSegmentsValid(Qt::Horizontal)) + createScrollingSegments(releaseVelocity.x(), contentPosition.x() + overshootPosition.x(), ppm.x(), Qt::Horizontal); + + if (forceRecalc || !scrollingSegmentsValid(Qt::Vertical)) + createScrollingSegments(releaseVelocity.y(), contentPosition.y() + overshootPosition.y(), ppm.y(), Qt::Vertical); +} + +/*! \internal + Returns the end position after the current scroll has finished. +*/ +qreal QScrollerPrivate::scrollingSegmentsEndPos(Qt::Orientation orientation) const +{ + if (orientation == Qt::Horizontal) { + if (xSegments.isEmpty()) + return contentPosition.x() + overshootPosition.x(); + else + return xSegments.last().stopPos; + } else { + if (ySegments.isEmpty()) + return contentPosition.y() + overshootPosition.y(); + else + return ySegments.last().stopPos; + } +} + +/*! \internal + Checks if the scroller segment end in a valid position. +*/ +bool QScrollerPrivate::scrollingSegmentsValid(Qt::Orientation orientation) +{ + QQueue<ScrollSegment> *segments; + qreal minPos; + qreal maxPos; + + if (orientation == Qt::Horizontal) { + segments = &xSegments; + minPos = contentPosRange.left(); + maxPos = contentPosRange.right(); + } else { + segments = &ySegments; + minPos = contentPosRange.top(); + maxPos = contentPosRange.bottom(); + } + + if (segments->isEmpty()) + return true; + + const ScrollSegment &last = segments->last(); + qreal stopPos = last.stopPos; + + if (last.type == ScrollTypeScrollTo) + return true; // scrollTo is always valid + + if (last.type == ScrollTypeOvershoot && + (stopPos != minPos && stopPos != maxPos)) + return false; + + if (stopPos < minPos || stopPos > maxPos) + return false; + + if (stopPos == minPos || stopPos == maxPos) // the begin and the end of the list are always ok + return true; + + qreal nextSnap = nextSnapPos(stopPos, 0, orientation); + if (!qIsNaN(nextSnap) && stopPos != nextSnap) + return false; + + return true; +} + +/*! \internal + Creates the sections needed to scroll to the specific \a endPos to the segments queue. +*/ +void QScrollerPrivate::createScrollToSegments(qreal v, qreal deltaTime, qreal endPos, Qt::Orientation orientation, ScrollType type) +{ + Q_UNUSED(v); + + if (orientation == Qt::Horizontal) + xSegments.clear(); + else + ySegments.clear(); + + qScrollerDebug() << "+++ createScrollToSegments: t:" << deltaTime << "ep:" << endPos << "o:" << int(orientation); + + const QScrollerPropertiesPrivate *sp = properties.d.data(); + + qreal startPos = (orientation == Qt::Horizontal) ? contentPosition.x() + overshootPosition.x() + : contentPosition.y() + overshootPosition.y(); + qreal deltaPos = (endPos - startPos) / 2; + + pushSegment(type, deltaTime * qreal(0.3), qreal(1.0), startPos, deltaPos, startPos + deltaPos, QEasingCurve::InQuad, orientation); + pushSegment(type, deltaTime * qreal(0.7), qreal(1.0), startPos + deltaPos, deltaPos, endPos, sp->scrollingCurve.type(), orientation); +} + +/*! \internal +*/ +void QScrollerPrivate::createScrollingSegments(qreal v, qreal startPos, qreal ppm, Qt::Orientation orientation) +{ + const QScrollerPropertiesPrivate *sp = properties.d.data(); + + QScrollerProperties::OvershootPolicy policy; + qreal minPos; + qreal maxPos; + qreal viewSize; + + if (orientation == Qt::Horizontal) { + xSegments.clear(); + policy = sp->hOvershootPolicy; + minPos = contentPosRange.left(); + maxPos = contentPosRange.right(); + viewSize = viewportSize.width(); + } else { + ySegments.clear(); + policy = sp->vOvershootPolicy; + minPos = contentPosRange.top(); + maxPos = contentPosRange.bottom(); + viewSize = viewportSize.height(); + } + + bool alwaysOvershoot = (policy == QScrollerProperties::OvershootAlwaysOn); + bool noOvershoot = (policy == QScrollerProperties::OvershootAlwaysOff) || !sp->overshootScrollDistanceFactor; + bool canOvershoot = !noOvershoot && (alwaysOvershoot || maxPos); + + qScrollerDebug() << "+++ createScrollingSegments: s:" << startPos << "maxPos:" << maxPos << "o:" << int(orientation); + + qScrollerDebug() << "v = " << v << ", decelerationFactor = " << sp->decelerationFactor << ", curveType = " << sp->scrollingCurve.type(); + + // This is only correct for QEasingCurve::OutQuad (linear velocity, + // constant deceleration), but the results look and feel ok for OutExpo + // and OutSine as well + + // v(t) = deltaTime * a * 0.5 * differentialForProgress(t / deltaTime) + // v(0) = vrelease + // v(deltaTime) = 0 + // deltaTime = (2 * vrelease) / (a * differntial(0)) + + // pos(t) = integrate(v(t)dt) + // pos(t) = vrelease * t - 0.5 * a * t * t + // pos(t) = deltaTime * a * 0.5 * progress(t / deltaTime) * deltaTime + // deltaPos = pos(deltaTime) + + qreal deltaTime = (qreal(2) * qAbs(v)) / (sp->decelerationFactor * differentialForProgress(sp->scrollingCurve, 0)); + qreal deltaPos = qSign(v) * deltaTime * deltaTime * qreal(0.5) * sp->decelerationFactor * ppm; + qreal endPos = startPos + deltaPos; + + qScrollerDebug() << " Real Delta:" << deltaPos; + + // -- determine snap points + qreal nextSnap = nextSnapPos(endPos, 0, orientation); + qreal lowerSnapPos = nextSnapPos(startPos, -1, orientation); + qreal higherSnapPos = nextSnapPos(startPos, 1, orientation); + + qScrollerDebug() << " Real Delta:" << lowerSnapPos <<"-"<<nextSnap <<"-"<<higherSnapPos; + + // - check if we can reach another snap point + if (nextSnap > higherSnapPos || qIsNaN(higherSnapPos)) + higherSnapPos = nextSnap; + if (nextSnap < lowerSnapPos || qIsNaN(lowerSnapPos)) + lowerSnapPos = nextSnap; + + // -- check if are in overshoot and end in overshoot + if ((startPos < minPos && endPos < minPos) || + (startPos > maxPos && endPos > maxPos)) { + qreal stopPos = endPos < minPos ? minPos : maxPos; + qreal oDeltaTime = sp->overshootScrollTime; + + pushSegment(ScrollTypeOvershoot, oDeltaTime * qreal(0.7), qreal(1.0), startPos, stopPos - startPos, stopPos, sp->scrollingCurve.type(), orientation); + return; + } + + if (qAbs(v) < sp->minimumVelocity) { + + qScrollerDebug() << "### below minimum Vel" << orientation; + + // - no snap points or already at one + if (qIsNaN(nextSnap) || nextSnap == startPos) + return; // nothing to do, no scrolling needed. + + // - decide which point to use + + qreal snapDistance = higherSnapPos - lowerSnapPos; + + qreal pressDistance = (orientation == Qt::Horizontal) ? + lastPosition.x() - pressPosition.x() : + lastPosition.y() - pressPosition.y(); + + // if not dragged far enough, pick the next snap point. + if (sp->snapPositionRatio == 0.0 || qAbs(pressDistance / sp->snapPositionRatio) > snapDistance) + endPos = nextSnap; + else if (pressDistance < 0.0) + endPos = lowerSnapPos; + else + endPos = higherSnapPos; + + deltaPos = endPos - startPos; + qreal midPos = startPos + deltaPos * qreal(0.3); + pushSegment(ScrollTypeFlick, sp->snapTime * qreal(0.3), qreal(1.0), startPos, midPos - startPos, midPos, QEasingCurve::InQuad, orientation); + pushSegment(ScrollTypeFlick, sp->snapTime * qreal(0.7), qreal(1.0), midPos, endPos - midPos, endPos, sp->scrollingCurve.type(), orientation); + return; + } + + // - go to the next snappoint if there is one + if (v > 0 && !qIsNaN(higherSnapPos)) { + // change the time in relation to the changed end position + if (endPos - startPos) + deltaTime *= qAbs((higherSnapPos - startPos) / (endPos - startPos)); + if (deltaTime > sp->snapTime) + deltaTime = sp->snapTime; + endPos = higherSnapPos; + + } else if (v < 0 && !qIsNaN(lowerSnapPos)) { + // change the time in relation to the changed end position + if (endPos - startPos) + deltaTime *= qAbs((lowerSnapPos - startPos) / (endPos - startPos)); + if (deltaTime > sp->snapTime) + deltaTime = sp->snapTime; + endPos = lowerSnapPos; + + // -- check if we are overshooting + } else if (endPos < minPos || endPos > maxPos) { + qreal stopPos = endPos < minPos ? minPos : maxPos; + + qScrollerDebug() << "Overshoot: delta:" << (stopPos - startPos); + + qreal stopProgress = progressForValue(sp->scrollingCurve, qAbs((stopPos - startPos) / deltaPos)); + + if (!canOvershoot) { + qScrollerDebug() << "Overshoot stopp:" << stopProgress; + + pushSegment(ScrollTypeFlick, deltaTime, stopProgress, startPos, endPos, stopPos, sp->scrollingCurve.type(), orientation); + } else { + qreal oDeltaTime = sp->overshootScrollTime; + qreal oStopProgress = qMin(stopProgress + oDeltaTime * qreal(0.3) / deltaTime, qreal(1)); + qreal oDistance = startPos + deltaPos * sp->scrollingCurve.valueForProgress(oStopProgress) - stopPos; + qreal oMaxDistance = qSign(oDistance) * (viewSize * sp->overshootScrollDistanceFactor); + + qScrollerDebug() << "1 oDistance:" << oDistance << "Max:" << oMaxDistance << "stopP/oStopP" << stopProgress << oStopProgress; + + if (qAbs(oDistance) > qAbs(oMaxDistance)) { + oStopProgress = progressForValue(sp->scrollingCurve, qAbs((stopPos + oMaxDistance - startPos) / deltaPos)); + oDistance = oMaxDistance; + qScrollerDebug() << "2 oDistance:" << oDistance << "Max:" << oMaxDistance << "stopP/oStopP" << stopProgress << oStopProgress; + } + + pushSegment(ScrollTypeFlick, deltaTime, oStopProgress, startPos, deltaPos, stopPos + oDistance, sp->scrollingCurve.type(), orientation); + pushSegment(ScrollTypeOvershoot, oDeltaTime * qreal(0.7), qreal(1.0), stopPos + oDistance, -oDistance, stopPos, sp->scrollingCurve.type(), orientation); + } + return; + } + + pushSegment(ScrollTypeFlick, deltaTime, qreal(1.0), startPos, deltaPos, endPos, sp->scrollingCurve.type(), orientation); +} + + +/*! \internal + Prepares scrolling by sending a QScrollPrepareEvent to the receiver widget. + Returns true if the scrolling was accepted and a target was returned. +*/ +bool QScrollerPrivate::prepareScrolling(const QPointF &position) +{ + QScrollPrepareEvent spe(position); + spe.ignore(); + sendEvent(target, &spe); + + qScrollerDebug() << "QScrollPrepareEvent returned from" << target << "with" << spe.isAccepted() << "mcp:" << spe.contentPosRange() << "cp:" << spe.contentPos(); + if (spe.isAccepted()) { + QPointF oldContentPos = contentPosition + overshootPosition; + QPointF contentDelta = spe.contentPos() - oldContentPos; + + viewportSize = spe.viewportSize(); + contentPosRange = spe.contentPosRange(); + if (contentPosRange.width() < 0) + contentPosRange.setWidth(0); + if (contentPosRange.height() < 0) + contentPosRange.setHeight(0); + contentPosition = clampToRect(spe.contentPos(), contentPosRange); + overshootPosition = spe.contentPos() - contentPosition; + + // - check if the content position was moved + if (contentDelta != QPointF(0, 0)) { + // need to correct all segments + for (int i = 0; i < xSegments.count(); i++) + xSegments[i].startPos -= contentDelta.x(); + + for (int i = 0; i < ySegments.count(); i++) + ySegments[i].startPos -= contentDelta.y(); + } + + if (QWidget *w = qobject_cast<QWidget *>(target)) + setDpiFromWidget(w); +#ifndef QT_NO_GRAPHICSVIEW + if (QGraphicsObject *go = qobject_cast<QGraphicsObject *>(target)) { + //TODO: the first view isn't really correct - maybe use an additional field in the prepare event? + if (go->scene() && !go->scene()->views().isEmpty()) + setDpiFromWidget(go->scene()->views().first()); + } +#endif + + if (state == QScroller::Scrolling) { + recalcScrollingSegments(); + } + return true; + } + + return false; +} + +void QScrollerPrivate::handleDrag(const QPointF &position, qint64 timestamp) +{ + const QScrollerPropertiesPrivate *sp = properties.d.data(); + + QPointF deltaPixel = position - lastPosition; + qint64 deltaTime = timestamp - lastTimestamp; + + if (sp->axisLockThreshold) { + int dx = qAbs(deltaPixel.x()); + int dy = qAbs(deltaPixel.y()); + if (dx || dy) { + bool vertical = (dy > dx); + qreal alpha = qreal(vertical ? dx : dy) / qreal(vertical ? dy : dx); + //qScrollerDebug() << "QScroller::handleDrag() -- axis lock:" << alpha << " / " << axisLockThreshold << "- isvertical:" << vertical << "- dx:" << dx << "- dy:" << dy; + if (alpha <= sp->axisLockThreshold) { + if (vertical) + deltaPixel.setX(0); + else + deltaPixel.setY(0); + } + } + } + + // calculate velocity (if the user would release the mouse NOW) + updateVelocity(deltaPixel, deltaTime); + + // restrict velocity, if content is not scrollable + QRectF max = contentPosRange; + bool canScrollX = (max.width() > 0) || (sp->hOvershootPolicy == QScrollerProperties::OvershootAlwaysOn); + bool canScrollY = (max.height() > 0) || (sp->vOvershootPolicy == QScrollerProperties::OvershootAlwaysOn); + + if (!canScrollX) { + deltaPixel.setX(0); + releaseVelocity.setX(0); + } + if (!canScrollY) { + deltaPixel.setY(0); + releaseVelocity.setY(0); + } + +// if (firstDrag) { +// // Do not delay the first drag +// setContentPositionHelper(q->contentPosition() - overshootDistance - deltaPixel); +// dragDistance = QPointF(0, 0); +// } else { + dragDistance += deltaPixel; +// } +//qScrollerDebug() << "######################" << deltaPixel << position.y() << lastPosition.y(); + if (canScrollX) + lastPosition.setX(position.x()); + if (canScrollY) + lastPosition.setY(position.y()); + lastTimestamp = timestamp; +} + +bool QScrollerPrivate::pressWhileInactive(const QPointF &position, qint64 timestamp) +{ + if (prepareScrolling(position)) { + const QScrollerPropertiesPrivate *sp = properties.d.data(); + + if (!contentPosRange.isNull() || + (sp->hOvershootPolicy == QScrollerProperties::OvershootAlwaysOn) || + (sp->vOvershootPolicy == QScrollerProperties::OvershootAlwaysOn)) { + + lastPosition = pressPosition = position; + lastTimestamp = pressTimestamp = timestamp; + setState(QScroller::Pressed); + } + } + return false; +} + +bool QScrollerPrivate::releaseWhilePressed(const QPointF &, qint64) +{ + if (overshootPosition != QPointF(0.0, 0.0)) { + setState(QScroller::Scrolling); + return true; + } else { + setState(QScroller::Inactive); + return false; + } +} + +bool QScrollerPrivate::moveWhilePressed(const QPointF &position, qint64 timestamp) +{ + Q_Q(QScroller); + const QScrollerPropertiesPrivate *sp = properties.d.data(); + QPointF ppm = q->pixelPerMeter(); + + QPointF deltaPixel = position - pressPosition; + + bool moveAborted = false; + bool moveStarted = (((deltaPixel / ppm).manhattanLength()) > sp->dragStartDistance); + + // check the direction of the mouse drag and abort if it's too much in the wrong direction. + if (moveStarted) { + QRectF max = contentPosRange; + bool canScrollX = (max.width() > 0); + bool canScrollY = (max.height() > 0); + + if (sp->hOvershootPolicy == QScrollerProperties::OvershootAlwaysOn) + canScrollX = true; + if (sp->vOvershootPolicy == QScrollerProperties::OvershootAlwaysOn) + canScrollY = true; + + if (qAbs(deltaPixel.x() / ppm.x()) < qAbs(deltaPixel.y() / ppm.y())) { + if (!canScrollY) + moveAborted = true; + } else { + if (!canScrollX) + moveAborted = true; + } + } + + if (moveAborted) { + setState(QScroller::Inactive); + moveStarted = false; + + } else if (moveStarted) { + setState(QScroller::Dragging); + + // subtract the dragStartDistance + deltaPixel = deltaPixel - deltaPixel * (sp->dragStartDistance / deltaPixel.manhattanLength()); + + if (deltaPixel != QPointF(0, 0)) { + // handleDrag updates lastPosition, lastTimestamp and velocity + handleDrag(pressPosition + deltaPixel, timestamp); + } + } + return moveStarted; +} + +bool QScrollerPrivate::moveWhileDragging(const QPointF &position, qint64 timestamp) +{ + // handleDrag updates lastPosition, lastTimestamp and velocity + handleDrag(position, timestamp); + return true; +} + +void QScrollerPrivate::timerEventWhileDragging() +{ + if (dragDistance != QPointF(0, 0)) { + qScrollerDebug() << "QScroller::timerEventWhileDragging() -- dragDistance:" << dragDistance; + + setContentPositionHelperDragging(-dragDistance); + dragDistance = QPointF(0, 0); + } +} + +bool QScrollerPrivate::releaseWhileDragging(const QPointF &position, qint64 timestamp) +{ + Q_Q(QScroller); + const QScrollerPropertiesPrivate *sp = properties.d.data(); + + // handleDrag updates lastPosition, lastTimestamp and velocity + handleDrag(position, timestamp); + + // check if we moved at all - this can happen if you stop a running + // scroller with a press and release shortly afterwards + QPointF deltaPixel = position - pressPosition; + if (((deltaPixel / q->pixelPerMeter()).manhattanLength()) > sp->dragStartDistance) { + + // handle accelerating flicks + if ((oldVelocity != QPointF(0, 0)) && sp->acceleratingFlickMaximumTime && + ((timestamp - pressTimestamp) < qint64(sp->acceleratingFlickMaximumTime * 1000))) { + + // - determine if the direction was changed + int signX = 0, signY = 0; + if (releaseVelocity.x()) + signX = (releaseVelocity.x() > 0) == (oldVelocity.x() > 0) ? 1 : -1; + if (releaseVelocity.y()) + signY = (releaseVelocity.y() > 0) == (oldVelocity.y() > 0) ? 1 : -1; + + if (signX > 0) + releaseVelocity.setX(qBound(-sp->maximumVelocity, + oldVelocity.x() * sp->acceleratingFlickSpeedupFactor, + sp->maximumVelocity)); + if (signY > 0) + releaseVelocity.setY(qBound(-sp->maximumVelocity, + oldVelocity.y() * sp->acceleratingFlickSpeedupFactor, + sp->maximumVelocity)); + } + } + + QPointF ppm = q->pixelPerMeter(); + createScrollingSegments(releaseVelocity.x(), contentPosition.x() + overshootPosition.x(), ppm.x(), Qt::Horizontal); + createScrollingSegments(releaseVelocity.y(), contentPosition.y() + overshootPosition.y(), ppm.y(), Qt::Vertical); + + qScrollerDebug() << "QScroller::releaseWhileDragging() -- velocity:" << releaseVelocity << "-- minimum velocity:" << sp->minimumVelocity << "overshoot" << overshootPosition; + + if (xSegments.isEmpty() && ySegments.isEmpty()) + setState(QScroller::Inactive); + else + setState(QScroller::Scrolling); + + return true; +} + +void QScrollerPrivate::timerEventWhileScrolling() +{ + qScrollerDebug() << "QScroller::timerEventWhileScrolling()"; + + setContentPositionHelperScrolling(); + if (xSegments.isEmpty() && ySegments.isEmpty()) + setState(QScroller::Inactive); +} + +bool QScrollerPrivate::pressWhileScrolling(const QPointF &position, qint64 timestamp) +{ + Q_Q(QScroller); + + if ((q->velocity() <= properties.d->maximumClickThroughVelocity) && + (overshootPosition == QPointF(0.0, 0.0))) { + setState(QScroller::Inactive); + return false; + } else { + lastPosition = pressPosition = position; + lastTimestamp = pressTimestamp = timestamp; + setState(QScroller::Pressed); + setState(QScroller::Dragging); + return true; + } +} + +/*! \internal + This function handles all state changes of the scroller. +*/ +void QScrollerPrivate::setState(QScroller::State newstate) +{ + Q_Q(QScroller); + bool sendLastScroll = false; + + if (state == newstate) + return; + + qScrollerDebug() << q << "QScroller::setState(" << stateName(newstate) << ")"; + + switch (newstate) { + case QScroller::Inactive: +#ifndef QT_NO_ANIMATION + scrollTimer->stop(); +#endif + + // send the last scroll event (but only after the current state change was finished) + if (!firstScroll) + sendLastScroll = true; + + releaseVelocity = QPointF(0, 0); + break; + + case QScroller::Pressed: +#ifndef QT_NO_ANIMATION + scrollTimer->stop(); +#endif + + oldVelocity = releaseVelocity; + releaseVelocity = QPointF(0, 0); + break; + + case QScroller::Dragging: + dragDistance = QPointF(0, 0); +#ifndef QT_NO_ANIMATION + if (state == QScroller::Pressed) + scrollTimer->start(); +#endif + break; + + case QScroller::Scrolling: +#ifndef QT_NO_ANIMATION + scrollTimer->start(); +#endif + break; + } + + qSwap(state, newstate); + + if (sendLastScroll) { + QScrollEvent se(contentPosition, overshootPosition, QScrollEvent::ScrollFinished); + sendEvent(target, &se); + firstScroll = true; + } + if (state == QScroller::Dragging || state == QScroller::Scrolling) + qt_activeScrollers()->insert(q); + else + qt_activeScrollers()->remove(q); + emit q->stateChanged(state); +} + + +/*! \internal + Helps when setting the content position. + It will try to move the content by the requested delta but stop in case + when we are coming back from an overshoot or a scrollTo. + It will also indicate a new overshooting condition by the overshootX and oversthootY flags. + + In this cases it will reset the velocity variables and other flags. + + Also keeps track of the current over-shooting value in overshootPosition. + + \a deltaPos is the amount of pixels the current content position should be moved +*/ +void QScrollerPrivate::setContentPositionHelperDragging(const QPointF &deltaPos) +{ + Q_Q(QScroller); + QPointF ppm = q->pixelPerMeter(); + const QScrollerPropertiesPrivate *sp = properties.d.data(); + QPointF v = q->velocity(); + + if (sp->overshootDragResistanceFactor) + overshootPosition /= sp->overshootDragResistanceFactor; + + QPointF oldPos = contentPosition + overshootPosition; + QPointF newPos = oldPos + deltaPos; + + qScrollerDebug() << "QScroller::setContentPositionHelperDragging(" << deltaPos << " [pix])"; + qScrollerDebug() << " --> overshoot:" << overshootPosition << "- old pos:" << oldPos << "- new pos:" << newPos; + + QPointF oldClampedPos = clampToRect(oldPos, contentPosRange); + QPointF newClampedPos = clampToRect(newPos, contentPosRange); + + // --- handle overshooting and stop if the coordinate is going back inside the normal area + bool alwaysOvershootX = (sp->hOvershootPolicy == QScrollerProperties::OvershootAlwaysOn); + bool alwaysOvershootY = (sp->vOvershootPolicy == QScrollerProperties::OvershootAlwaysOn); + bool noOvershootX = (sp->hOvershootPolicy == QScrollerProperties::OvershootAlwaysOff) || + ((state == QScroller::Dragging) && !sp->overshootDragResistanceFactor) || + !sp->overshootDragDistanceFactor; + bool noOvershootY = (sp->vOvershootPolicy == QScrollerProperties::OvershootAlwaysOff) || + ((state == QScroller::Dragging) && !sp->overshootDragResistanceFactor) || + !sp->overshootDragDistanceFactor; + bool canOvershootX = !noOvershootX && (alwaysOvershootX || contentPosRange.width()); + bool canOvershootY = !noOvershootY && (alwaysOvershootY || contentPosRange.height()); + + qreal oldOvershootX = (canOvershootX) ? oldPos.x() - oldClampedPos.x() : 0; + qreal oldOvershootY = (canOvershootY) ? oldPos.y() - oldClampedPos.y() : 0; + + qreal newOvershootX = (canOvershootX) ? newPos.x() - newClampedPos.x() : 0; + qreal newOvershootY = (canOvershootY) ? newPos.y() - newClampedPos.y() : 0; + + qreal maxOvershootX = viewportSize.width() * sp->overshootDragDistanceFactor; + qreal maxOvershootY = viewportSize.height() * sp->overshootDragDistanceFactor; + + qScrollerDebug() << " --> noOs:" << noOvershootX << "drf:" << sp->overshootDragResistanceFactor << "mdf:" << sp->overshootScrollDistanceFactor << "ossP:"<<sp->hOvershootPolicy; + qScrollerDebug() << " --> canOS:" << canOvershootX << "newOS:" << newOvershootX << "maxOS:" << maxOvershootX; + + if (sp->overshootDragResistanceFactor) { + oldOvershootX *= sp->overshootDragResistanceFactor; + oldOvershootY *= sp->overshootDragResistanceFactor; + newOvershootX *= sp->overshootDragResistanceFactor; + newOvershootY *= sp->overshootDragResistanceFactor; + } + + // -- stop at the maximum overshoot distance + + newOvershootX = qBound(-maxOvershootX, newOvershootX, maxOvershootX); + newOvershootY = qBound(-maxOvershootY, newOvershootY, maxOvershootY); + + overshootPosition.setX(newOvershootX); + overshootPosition.setY(newOvershootY); + contentPosition = newClampedPos; + + QScrollEvent se(contentPosition, overshootPosition, firstScroll ? QScrollEvent::ScrollStarted : QScrollEvent::ScrollUpdated); + sendEvent(target, &se); + firstScroll = false; + + qScrollerDebug() << " --> new position:" << newClampedPos << "- new overshoot:" << overshootPosition << + "- overshoot x/y?:" << overshootPosition; +} + + +qreal QScrollerPrivate::nextSegmentPosition(QQueue<ScrollSegment> &segments, qint64 now, qreal oldPos) +{ + qreal pos = oldPos; + + // check the X segments for new positions + while (!segments.isEmpty()) { + const ScrollSegment s = segments.head(); + + if ((s.startTime + s.deltaTime * s.stopProgress) <= now) { + segments.dequeue(); + pos = s.stopPos; + } else if (s.startTime <= now) { + qreal progress = qreal(now - s.startTime) / qreal(s.deltaTime); + pos = s.startPos + s.deltaPos * s.curve.valueForProgress(progress); + if (s.deltaPos > 0 ? pos > s.stopPos : pos < s.stopPos) { + segments.dequeue(); + pos = s.stopPos; + } else { + break; + } + } else { + break; + } + } + return pos; +} + +void QScrollerPrivate::setContentPositionHelperScrolling() +{ + qint64 now = monotonicTimer.elapsed(); + QPointF newPos = contentPosition + overshootPosition; + + newPos.setX(nextSegmentPosition(xSegments, now, newPos.x())); + newPos.setY(nextSegmentPosition(ySegments, now, newPos.y())); + + // -- set the position and handle overshoot + qScrollerDebug() << "QScroller::setContentPositionHelperScrolling()"; + qScrollerDebug() << " --> overshoot:" << overshootPosition << "- new pos:" << newPos; + + QPointF newClampedPos = clampToRect(newPos, contentPosRange); + + overshootPosition = newPos - newClampedPos; + contentPosition = newClampedPos; + + QScrollEvent se(contentPosition, overshootPosition, firstScroll ? QScrollEvent::ScrollStarted : QScrollEvent::ScrollUpdated); + sendEvent(target, &se); + firstScroll = false; + + qScrollerDebug() << " --> new position:" << newClampedPos << "- new overshoot:" << overshootPosition; +} + +/*! \internal + Returns the next snap point in direction. + If \a direction >0 it will return the next snap point that is larger than the current position. + If \a direction <0 it will return the next snap point that is smaller than the current position. + If \a direction ==0 it will return the nearest snap point (or the current position if we are already + on a snap point. + Returns the nearest snap position or NaN if no such point could be found. + */ +qreal QScrollerPrivate::nextSnapPos(qreal p, int dir, Qt::Orientation orientation) +{ + qreal bestSnapPos = Q_QNAN; + qreal bestSnapPosDist = Q_INFINITY; + + qreal minPos; + qreal maxPos; + + if (orientation == Qt::Horizontal) { + minPos = contentPosRange.left(); + maxPos = contentPosRange.right(); + } else { + minPos = contentPosRange.top(); + maxPos = contentPosRange.bottom(); + } + + if (orientation == Qt::Horizontal) { + // the snap points in the list + foreach (qreal snapPos, snapPositionsX) { + qreal snapPosDist = snapPos - p; + if ((dir > 0 && snapPosDist < 0) || + (dir < 0 && snapPosDist > 0)) + continue; // wrong direction + if (snapPos < minPos || snapPos > maxPos ) + continue; // invalid + + if (qIsNaN(bestSnapPos) || + qAbs(snapPosDist) < bestSnapPosDist ) { + bestSnapPos = snapPos; + bestSnapPosDist = qAbs(snapPosDist); + } + } + + // the snap point interval + if (snapIntervalX > 0.0) { + qreal first = minPos + snapFirstX; + qreal snapPos; + if (dir > 0) + snapPos = qCeil((p - first) / snapIntervalX) * snapIntervalX + first; + else if (dir < 0) + snapPos = qFloor((p - first) / snapIntervalX) * snapIntervalX + first; + else if (p <= first) + snapPos = first; + else + { + qreal last = qFloor((maxPos - first) / snapIntervalX) * snapIntervalX + first; + if (p >= last) + snapPos = last; + else + snapPos = qRound((p - first) / snapIntervalX) * snapIntervalX + first; + } + + if (snapPos >= first && snapPos <= maxPos ) { + qreal snapPosDist = snapPos - p; + + if (qIsNaN(bestSnapPos) || + qAbs(snapPosDist) < bestSnapPosDist ) { + bestSnapPos = snapPos; + bestSnapPosDist = qAbs(snapPosDist); + } + } + } + + } else { // (orientation == Qt::Vertical) + // the snap points in the list + foreach (qreal snapPos, snapPositionsY) { + qreal snapPosDist = snapPos - p; + if ((dir > 0 && snapPosDist < 0) || + (dir < 0 && snapPosDist > 0)) + continue; // wrong direction + if (snapPos < minPos || snapPos > maxPos ) + continue; // invalid + + if (qIsNaN(bestSnapPos) || + qAbs(snapPosDist) < bestSnapPosDist) { + bestSnapPos = snapPos; + bestSnapPosDist = qAbs(snapPosDist); + } + } + + // the snap point interval + if (snapIntervalY > 0.0) { + qreal first = minPos + snapFirstY; + qreal snapPos; + if (dir > 0) + snapPos = qCeil((p - first) / snapIntervalY) * snapIntervalY + first; + else if (dir < 0) + snapPos = qFloor((p - first) / snapIntervalY) * snapIntervalY + first; + else if (p <= first) + snapPos = first; + else + { + qreal last = qFloor((maxPos - first) / snapIntervalY) * snapIntervalY + first; + if (p >= last) + snapPos = last; + else + snapPos = qRound((p - first) / snapIntervalY) * snapIntervalY + first; + } + + if (snapPos >= first && snapPos <= maxPos ) { + qreal snapPosDist = snapPos - p; + + if (qIsNaN(bestSnapPos) || + qAbs(snapPosDist) < bestSnapPosDist) { + bestSnapPos = snapPos; + bestSnapPosDist = qAbs(snapPosDist); + } + } + } + } + + return bestSnapPos; +} + +/*! + \enum QScroller::State + + This enum contains the different QScroller states. + + \value Inactive The scroller is not scrolling and nothing is pressed. + \value Pressed A touch event was received or the mouse button was pressed but the scroll area is currently not dragged. + \value Dragging The scroll area is currently following the touch point or mouse. + \value Scrolling The scroll area is moving on it's own. +*/ + +/*! + \enum QScroller::ScrollerGestureType + + This enum contains the different gesture types that are supported by the QScroller gesture recognizer. + + \value TouchGesture The gesture recognizer will only trigger on touch + events. Specifically it will react on single touch points when using a + touch screen and dual touch points when using a touchpad. + \value LeftMouseButtonGesture The gesture recognizer will only trigger on left mouse button events. + \value MiddleMouseButtonGesture The gesture recognizer will only trigger on middle mouse button events. + \value RightMouseButtonGesture The gesture recognizer will only trigger on right mouse button events. +*/ + +/*! + \enum QScroller::Input + + This enum contains an input device agnostic view of input events that are relevant for QScroller. + + \value InputPress The user pressed the input device (e.g. QEvent::MouseButtonPress, + QEvent::GraphicsSceneMousePress, QEvent::TouchBegin) + + \value InputMove The user moved the input device (e.g. QEvent::MouseMove, + QEvent::GraphicsSceneMouseMove, QEvent::TouchUpdate) + + \value InputRelease The user released the input device (e.g. QEvent::MouseButtonRelease, + QEvent::GraphicsSceneMouseRelease, QEvent::TouchEnd) + +*/ + +QT_END_NAMESPACE diff --git a/src/widgets/util/qscroller.h b/src/widgets/util/qscroller.h new file mode 100644 index 0000000000..1599c7dcdf --- /dev/null +++ b/src/widgets/util/qscroller.h @@ -0,0 +1,155 @@ +/**************************************************************************** +** +** Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies). +** All rights reserved. +** Contact: Nokia Corporation (qt-info@nokia.com) +** +** This file is part of the QtGui module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** No Commercial Usage +** This file contains pre-release code and may not be distributed. +** You may use this file in accordance with the terms and conditions +** contained in the Technology Preview License Agreement accompanying +** this package. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain additional +** rights. These rights are described in the Nokia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** If you have questions regarding the use of this file, please contact +** Nokia at qt-info@nokia.com. +** +** +** +** +** +** +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QSCROLLER_H +#define QSCROLLER_H + +#include <QtCore/QObject> +#include <QtCore/QPointF> +#include <QtGui/QScrollerProperties> + +QT_BEGIN_HEADER + +QT_BEGIN_NAMESPACE + +QT_MODULE(Gui) + +class QWidget; +class QScrollerPrivate; +class QScrollerProperties; +#ifndef QT_NO_GESTURES +class QFlickGestureRecognizer; +class QMouseFlickGestureRecognizer; +#endif + +class Q_GUI_EXPORT QScroller : public QObject +{ + Q_OBJECT + Q_PROPERTY(State state READ state NOTIFY stateChanged) + Q_PROPERTY(QScrollerProperties scrollerProperties READ scrollerProperties WRITE setScrollerProperties NOTIFY scrollerPropertiesChanged) + Q_ENUMS(State) + +public: + enum State + { + Inactive, + Pressed, + Dragging, + Scrolling + }; + + enum ScrollerGestureType + { + TouchGesture, + LeftMouseButtonGesture, + RightMouseButtonGesture, + MiddleMouseButtonGesture + }; + + enum Input + { + InputPress = 1, + InputMove, + InputRelease + }; + + static bool hasScroller(QObject *target); + + static QScroller *scroller(QObject *target); + static const QScroller *scroller(const QObject *target); + +#ifndef QT_NO_GESTURES + static Qt::GestureType grabGesture(QObject *target, ScrollerGestureType gestureType = TouchGesture); + static Qt::GestureType grabbedGesture(QObject *target); + static void ungrabGesture(QObject *target); +#endif + + static QList<QScroller *> activeScrollers(); + + QObject *target() const; + + State state() const; + + bool handleInput(Input input, const QPointF &position, qint64 timestamp = 0); + + void stop(); + QPointF velocity() const; + QPointF finalPosition() const; + QPointF pixelPerMeter() const; + + QScrollerProperties scrollerProperties() const; + + void setSnapPositionsX( const QList<qreal> &positions ); + void setSnapPositionsX( qreal first, qreal interval ); + void setSnapPositionsY( const QList<qreal> &positions ); + void setSnapPositionsY( qreal first, qreal interval ); + +public Q_SLOTS: + void setScrollerProperties(const QScrollerProperties &prop); + void scrollTo(const QPointF &pos); + void scrollTo(const QPointF &pos, int scrollTime); + void ensureVisible(const QRectF &rect, qreal xmargin, qreal ymargin); + void ensureVisible(const QRectF &rect, qreal xmargin, qreal ymargin, int scrollTime); + void resendPrepareEvent(); + +Q_SIGNALS: + void stateChanged(QScroller::State newstate); + void scrollerPropertiesChanged(const QScrollerProperties &); + +private: + QScrollerPrivate *d_ptr; + + QScroller(QObject *target); + virtual ~QScroller(); + + Q_DISABLE_COPY(QScroller) + Q_DECLARE_PRIVATE(QScroller) + +#ifndef QT_NO_GESTURES + friend class QFlickGestureRecognizer; +#endif +}; + +QT_END_NAMESPACE + +QT_END_HEADER + +#endif // QSCROLLER_H diff --git a/src/widgets/util/qscroller_mac.mm b/src/widgets/util/qscroller_mac.mm new file mode 100644 index 0000000000..4bf69c17c0 --- /dev/null +++ b/src/widgets/util/qscroller_mac.mm @@ -0,0 +1,71 @@ +/**************************************************************************** +** +** Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies). +** All rights reserved. +** Contact: Nokia Corporation (qt-info@nokia.com) +** +** This file is part of the QtGui module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** No Commercial Usage +** This file contains pre-release code and may not be distributed. +** You may use this file in accordance with the terms and conditions +** contained in the Technology Preview License Agreement accompanying +** this package. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain additional +** rights. These rights are described in the Nokia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** If you have questions regarding the use of this file, please contact +** Nokia at qt-info@nokia.com. +** +** +** +** +** +** +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include <QtCore/qglobal.h> + +#ifdef Q_WS_MAC + +#import <Cocoa/Cocoa.h> + +#include "qscroller_p.h" + +QPointF QScrollerPrivate::realDpi(int screen) +{ + NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; + NSArray *nsscreens = [NSScreen screens]; + + if (screen < 0 || screen >= int([nsscreens count])) + screen = 0; + + NSScreen *nsscreen = [nsscreens objectAtIndex:screen]; + CGDirectDisplayID display = [[[nsscreen deviceDescription] objectForKey:@"NSScreenNumber"] intValue]; + + CGSize mmsize = CGDisplayScreenSize(display); + if (mmsize.width > 0 && mmsize.height > 0) { + return QPointF(CGDisplayPixelsWide(display) / mmsize.width, + CGDisplayPixelsHigh(display) / mmsize.height) * qreal(25.4); + } else { + return QPointF(); + } + [pool release]; +} + +#endif diff --git a/src/widgets/util/qscroller_p.h b/src/widgets/util/qscroller_p.h new file mode 100644 index 0000000000..c1196152df --- /dev/null +++ b/src/widgets/util/qscroller_p.h @@ -0,0 +1,209 @@ +/**************************************************************************** +** +** Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies). +** All rights reserved. +** Contact: Nokia Corporation (qt-info@nokia.com) +** +** This file is part of the QtGui module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** No Commercial Usage +** This file contains pre-release code and may not be distributed. +** You may use this file in accordance with the terms and conditions +** contained in the Technology Preview License Agreement accompanying +** this package. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain additional +** rights. These rights are described in the Nokia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** If you have questions regarding the use of this file, please contact +** Nokia at qt-info@nokia.com. +** +** +** +** +** +** +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QSCROLLER_P_H +#define QSCROLLER_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QObject> +#include <QPointer> +#include <QQueue> +#include <QSet> +#include <QEasingCurve> +#include <QElapsedTimer> +#include <QSizeF> +#include <QPointF> +#include <QRectF> +#include <qscroller.h> +#include <qscrollerproperties.h> +#include <private/qscrollerproperties_p.h> +#include <QAbstractAnimation> + +QT_BEGIN_NAMESPACE + +#ifndef QT_NO_GESTURES +class QFlickGestureRecognizer; +#endif + +#ifndef QT_NO_ANIMATION +class QScrollTimer; +#endif +class QScrollerPrivate : public QObject +{ + Q_OBJECT + Q_DECLARE_PUBLIC(QScroller) + +public: + QScrollerPrivate(QScroller *q, QObject *target); + void init(); + + void sendEvent(QObject *o, QEvent *e); + + void setState(QScroller::State s); + + enum ScrollType { + ScrollTypeFlick = 0, + ScrollTypeScrollTo, + ScrollTypeOvershoot + }; + + struct ScrollSegment { + qint64 startTime; + qint64 deltaTime; + qreal startPos; + qreal deltaPos; + QEasingCurve curve; + qreal stopProgress; // whatever is.. + qreal stopPos; // ..reached first + ScrollType type; + }; + + bool pressWhileInactive(const QPointF &position, qint64 timestamp); + bool moveWhilePressed(const QPointF &position, qint64 timestamp); + bool releaseWhilePressed(const QPointF &position, qint64 timestamp); + bool moveWhileDragging(const QPointF &position, qint64 timestamp); + bool releaseWhileDragging(const QPointF &position, qint64 timestamp); + bool pressWhileScrolling(const QPointF &position, qint64 timestamp); + + void timerTick(); + void timerEventWhileDragging(); + void timerEventWhileScrolling(); + + bool prepareScrolling(const QPointF &position); + void handleDrag(const QPointF &position, qint64 timestamp); + + QPointF realDpi(int screen); + QPointF dpi() const; + void setDpi(const QPointF &dpi); + void setDpiFromWidget(QWidget *widget); + + void updateVelocity(const QPointF &deltaPixelRaw, qint64 deltaTime); + void pushSegment(ScrollType type, qreal deltaTime, qreal stopProgress, qreal startPos, qreal deltaPos, qreal stopPos, QEasingCurve::Type curve, Qt::Orientation orientation); + void recalcScrollingSegments(bool forceRecalc = false); + qreal scrollingSegmentsEndPos(Qt::Orientation orientation) const; + bool scrollingSegmentsValid(Qt::Orientation orientation); + void createScrollToSegments(qreal v, qreal deltaTime, qreal endPos, Qt::Orientation orientation, ScrollType type); + void createScrollingSegments(qreal v, qreal startPos, qreal ppm, Qt::Orientation orientation); + + void setContentPositionHelperDragging(const QPointF &deltaPos); + void setContentPositionHelperScrolling(); + + qreal nextSnapPos(qreal p, int dir, Qt::Orientation orientation); + static qreal nextSegmentPosition(QQueue<ScrollSegment> &segments, qint64 now, qreal oldPos); + + inline int frameRateSkip() const { return properties.d.data()->frameRate; } + + static const char *stateName(QScroller::State state); + static const char *inputName(QScroller::Input input); + +public slots: + void targetDestroyed(); + +public: + // non static + QObject *target; + QScrollerProperties properties; +#ifndef QT_NO_GESTURES + QFlickGestureRecognizer *recognizer; + Qt::GestureType recognizerType; +#endif + + // scroller state: + + // QPointer<QObject> scrollTarget; + QSizeF viewportSize; + QRectF contentPosRange; + QPointF contentPosition; + QPointF overshootPosition; // the number of pixels we are overshooting (before overshootDragResistanceFactor) + + // state + + bool enabled; + QScroller::State state; + bool firstScroll; // true if we haven't already send a scroll event + + QPointF oldVelocity; // the release velocity of the last drag + + QPointF pressPosition; + QPointF lastPosition; + qint64 pressTimestamp; + qint64 lastTimestamp; + + QPointF dragDistance; // the distance we should move during the next drag timer event + + QQueue<ScrollSegment> xSegments; + QQueue<ScrollSegment> ySegments; + + // snap positions + QList<qreal> snapPositionsX; + qreal snapFirstX; + qreal snapIntervalX; + QList<qreal> snapPositionsY; + qreal snapFirstY; + qreal snapIntervalY; + + QPointF pixelPerMeter; + + QElapsedTimer monotonicTimer; + + QPointF releaseVelocity; // the starting velocity of the scrolling state +#ifndef QT_NO_ANIMATION + QScrollTimer *scrollTimer; +#endif + + QScroller *q_ptr; +}; + + +QT_END_NAMESPACE + +#endif // QSCROLLER_P_H + diff --git a/src/widgets/util/qscrollerproperties.cpp b/src/widgets/util/qscrollerproperties.cpp new file mode 100644 index 0000000000..85e2e82af1 --- /dev/null +++ b/src/widgets/util/qscrollerproperties.cpp @@ -0,0 +1,393 @@ +/**************************************************************************** +** +** Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies). +** All rights reserved. +** Contact: Nokia Corporation (qt-info@nokia.com) +** +** This file is part of the QtGui module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** No Commercial Usage +** This file contains pre-release code and may not be distributed. +** You may use this file in accordance with the terms and conditions +** contained in the Technology Preview License Agreement accompanying +** this package. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain additional +** rights. These rights are described in the Nokia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** If you have questions regarding the use of this file, please contact +** Nokia at qt-info@nokia.com. +** +** +** +** +** +** +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include <QPointer> +#include <QObject> +#include <QtCore/qmath.h> +#ifdef Q_WS_WIN +# include <QLibrary> +#endif + +#include "qscrollerproperties.h" +#include "private/qscrollerproperties_p.h" + +QT_BEGIN_NAMESPACE + +static QScrollerPropertiesPrivate *userDefaults = 0; +static QScrollerPropertiesPrivate *systemDefaults = 0; + +QScrollerPropertiesPrivate *QScrollerPropertiesPrivate::defaults() +{ + if (!systemDefaults) { + QScrollerPropertiesPrivate spp; + spp.mousePressEventDelay = qreal(0.25); + spp.dragStartDistance = qreal(5.0 / 1000); + spp.dragVelocitySmoothingFactor = qreal(0.8); + spp.axisLockThreshold = qreal(0); + spp.scrollingCurve.setType(QEasingCurve::OutQuad); + spp.decelerationFactor = qreal(0.125); + spp.minimumVelocity = qreal(50.0 / 1000); + spp.maximumVelocity = qreal(500.0 / 1000); + spp.maximumClickThroughVelocity = qreal(66.5 / 1000); + spp.acceleratingFlickMaximumTime = qreal(1.25); + spp.acceleratingFlickSpeedupFactor = qreal(3.0); + spp.snapPositionRatio = qreal(0.5); + spp.snapTime = qreal(0.3); + spp.overshootDragResistanceFactor = qreal(0.5); + spp.overshootDragDistanceFactor = qreal(1); + spp.overshootScrollDistanceFactor = qreal(0.5); + spp.overshootScrollTime = qreal(0.7); +# ifdef Q_WS_WIN + if (QLibrary::resolve(QLatin1String("UxTheme"), "BeginPanningFeedback")) + spp.overshootScrollTime = qreal(0.35); +# endif + spp.hOvershootPolicy = QScrollerProperties::OvershootWhenScrollable; + spp.vOvershootPolicy = QScrollerProperties::OvershootWhenScrollable; + spp.frameRate = QScrollerProperties::Standard; + + systemDefaults = new QScrollerPropertiesPrivate(spp); + } + return new QScrollerPropertiesPrivate(userDefaults ? *userDefaults : *systemDefaults); +} + +/*! + \class QScrollerProperties + \brief The QScrollerProperties class stores the settings for a QScroller. + \since 4.8 + + The QScrollerProperties class stores the parameters used by QScroller. + + The default settings are platform dependent so that Qt emulates the + platform behaviour for kinetic scrolling. + + As a convention the QScrollerProperties are in physical units (meter, + seconds) and are converted by QScroller using the current DPI. + + \sa QScroller +*/ + +/*! + Constructs new scroller properties. +*/ +QScrollerProperties::QScrollerProperties() + : d(QScrollerPropertiesPrivate::defaults()) +{ +} + +/*! + Constructs a copy of \a sp. +*/ +QScrollerProperties::QScrollerProperties(const QScrollerProperties &sp) + : d(new QScrollerPropertiesPrivate(*sp.d)) +{ +} + +/*! + Assigns \a sp to these scroller properties and returns a reference to these scroller properties. +*/ +QScrollerProperties &QScrollerProperties::operator=(const QScrollerProperties &sp) +{ + *d.data() = *sp.d.data(); + return *this; +} + +/*! + Destroys the scroller properties. +*/ +QScrollerProperties::~QScrollerProperties() +{ +} + +/*! + Returns true if these scroller properties are equal to \a sp; otherwise returns false. +*/ +bool QScrollerProperties::operator==(const QScrollerProperties &sp) const +{ + return *d.data() == *sp.d.data(); +} + +/*! + Returns true if these scroller properties are different from \a sp; otherwise returns false. +*/ +bool QScrollerProperties::operator!=(const QScrollerProperties &sp) const +{ + return !(*d.data() == *sp.d.data()); +} + +bool QScrollerPropertiesPrivate::operator==(const QScrollerPropertiesPrivate &p) const +{ + bool same = true; + same &= (mousePressEventDelay == p.mousePressEventDelay); + same &= (dragStartDistance == p.dragStartDistance); + same &= (dragVelocitySmoothingFactor == p.dragVelocitySmoothingFactor); + same &= (axisLockThreshold == p.axisLockThreshold); + same &= (scrollingCurve == p.scrollingCurve); + same &= (decelerationFactor == p.decelerationFactor); + same &= (minimumVelocity == p.minimumVelocity); + same &= (maximumVelocity == p.maximumVelocity); + same &= (maximumClickThroughVelocity == p.maximumClickThroughVelocity); + same &= (acceleratingFlickMaximumTime == p.acceleratingFlickMaximumTime); + same &= (acceleratingFlickSpeedupFactor == p.acceleratingFlickSpeedupFactor); + same &= (snapPositionRatio == p.snapPositionRatio); + same &= (snapTime == p.snapTime); + same &= (overshootDragResistanceFactor == p.overshootDragResistanceFactor); + same &= (overshootDragDistanceFactor == p.overshootDragDistanceFactor); + same &= (overshootScrollDistanceFactor == p.overshootScrollDistanceFactor); + same &= (overshootScrollTime == p.overshootScrollTime); + same &= (hOvershootPolicy == p.hOvershootPolicy); + same &= (vOvershootPolicy == p.vOvershootPolicy); + same &= (frameRate == p.frameRate); + return same; +} + +/*! + Sets the scroller properties for all new QScrollerProperties objects to \a sp. + + Use this function to override the platform default properties returned by the default + constructor. If you only want to change the scroller properties of a single scroller, use + QScroller::setScrollerProperties() + + \note Calling this function will not change the content of already existing + QScrollerProperties objects. + + \sa unsetDefaultScrollerProperties() +*/ +void QScrollerProperties::setDefaultScrollerProperties(const QScrollerProperties &sp) +{ + if (!userDefaults) + userDefaults = new QScrollerPropertiesPrivate(*sp.d); + else + *userDefaults = *sp.d; +} + +/*! + Sets the scroller properties returned by the default constructor back to the platform default + properties. + + \sa setDefaultScrollerProperties() +*/ +void QScrollerProperties::unsetDefaultScrollerProperties() +{ + delete userDefaults; + userDefaults = 0; +} + +/*! + Query the \a metric value of the scroller properties. + + \sa setScrollMetric(), ScrollMetric +*/ +QVariant QScrollerProperties::scrollMetric(ScrollMetric metric) const +{ + switch (metric) { + case MousePressEventDelay: return d->mousePressEventDelay; + case DragStartDistance: return d->dragStartDistance; + case DragVelocitySmoothingFactor: return d->dragVelocitySmoothingFactor; + case AxisLockThreshold: return d->axisLockThreshold; + case ScrollingCurve: return d->scrollingCurve; + case DecelerationFactor: return d->decelerationFactor; + case MinimumVelocity: return d->minimumVelocity; + case MaximumVelocity: return d->maximumVelocity; + case MaximumClickThroughVelocity: return d->maximumClickThroughVelocity; + case AcceleratingFlickMaximumTime: return d->acceleratingFlickMaximumTime; + case AcceleratingFlickSpeedupFactor:return d->acceleratingFlickSpeedupFactor; + case SnapPositionRatio: return d->snapPositionRatio; + case SnapTime: return d->snapTime; + case OvershootDragResistanceFactor: return d->overshootDragResistanceFactor; + case OvershootDragDistanceFactor: return d->overshootDragDistanceFactor; + case OvershootScrollDistanceFactor: return d->overshootScrollDistanceFactor; + case OvershootScrollTime: return d->overshootScrollTime; + case HorizontalOvershootPolicy: return QVariant::fromValue(d->hOvershootPolicy); + case VerticalOvershootPolicy: return QVariant::fromValue(d->vOvershootPolicy); + case FrameRate: return QVariant::fromValue(d->frameRate); + case ScrollMetricCount: break; + } + return QVariant(); +} + +/*! + Set a specific value of the \a metric ScrollerMetric to \a value. + + \sa scrollMetric(), ScrollMetric +*/ +void QScrollerProperties::setScrollMetric(ScrollMetric metric, const QVariant &value) +{ + switch (metric) { + case MousePressEventDelay: d->mousePressEventDelay = value.toReal(); break; + case DragStartDistance: d->dragStartDistance = value.toReal(); break; + case DragVelocitySmoothingFactor: d->dragVelocitySmoothingFactor = qBound(qreal(0), value.toReal(), qreal(1)); break; + case AxisLockThreshold: d->axisLockThreshold = qBound(qreal(0), value.toReal(), qreal(1)); break; + case ScrollingCurve: d->scrollingCurve = value.toEasingCurve(); break; + case DecelerationFactor: d->decelerationFactor = value.toReal(); break; + case MinimumVelocity: d->minimumVelocity = value.toReal(); break; + case MaximumVelocity: d->maximumVelocity = value.toReal(); break; + case MaximumClickThroughVelocity: d->maximumClickThroughVelocity = value.toReal(); break; + case AcceleratingFlickMaximumTime: d->acceleratingFlickMaximumTime = value.toReal(); break; + case AcceleratingFlickSpeedupFactor:d->acceleratingFlickSpeedupFactor = value.toReal(); break; + case SnapPositionRatio: d->snapPositionRatio = qBound(qreal(0), value.toReal(), qreal(1)); break; + case SnapTime: d->snapTime = value.toReal(); break; + case OvershootDragResistanceFactor: d->overshootDragResistanceFactor = value.toReal(); break; + case OvershootDragDistanceFactor: d->overshootDragDistanceFactor = qBound(qreal(0), value.toReal(), qreal(1)); break; + case OvershootScrollDistanceFactor: d->overshootScrollDistanceFactor = qBound(qreal(0), value.toReal(), qreal(1)); break; + case OvershootScrollTime: d->overshootScrollTime = value.toReal(); break; + case HorizontalOvershootPolicy: d->hOvershootPolicy = value.value<QScrollerProperties::OvershootPolicy>(); break; + case VerticalOvershootPolicy: d->vOvershootPolicy = value.value<QScrollerProperties::OvershootPolicy>(); break; + case FrameRate: d->frameRate = value.value<QScrollerProperties::FrameRates>(); break; + case ScrollMetricCount: break; + } +} + +/*! + \enum QScrollerProperties::FrameRates + + This enum describes the available frame rates used while dragging or scrolling. + + \value Fps60 60 frames per second + \value Fps30 30 frames per second + \value Fps20 20 frames per second + \value Standard the default value is 60 frames per second (which corresponds to QAbstractAnimation). +*/ + +/*! + \enum QScrollerProperties::OvershootPolicy + + This enum describes the various modes of overshooting. + + \value OvershootWhenScrollable Overshooting is possible when the content is scrollable. This is the + default. + + \value OvershootAlwaysOff Overshooting is never enabled, even when the content is scrollable. + + \value OvershootAlwaysOn Overshooting is always enabled, even when the content is not + scrollable. +*/ + +/*! + \enum QScrollerProperties::ScrollMetric + + This enum contains the different scroll metric types. When not indicated otherwise the + setScrollMetric function expects a QVariant of type qreal. + + See the QScroller documentation for further details of the concepts behind the different + values. + + \value MousePressEventDelay This is the time a mouse press event is delayed when starting + a flick gesture in \c{[s]}. If the gesture is triggered within that time, no mouse press or + release is sent to the scrolled object. If it triggers after that delay the delayed + mouse press plus a faked release event at global postion \c{QPoint(-QWIDGETSIZE_MAX, + -QWIDGETSIZE_MAX)} is sent. If the gesture is canceled, then both the delayed mouse + press plus the real release event are delivered. + + \value DragStartDistance This is the minimum distance the touch or mouse point needs to be + moved before the flick gesture is triggered in \c m. + + \value DragVelocitySmoothingFactor A value that describes to which extent new drag velocities are + included in the final scrolling velocity. This value should be in the range between \c 0 and + \c 1. The lower the value, the more smoothing is applied to the dragging velocity. + + \value AxisLockThreshold Restricts the movement to one axis if the movement is inside an angle + around the axis. The threshold must be in the range \c 0 to \c 1. + + \value ScrollingCurve The QEasingCurve used when decelerating the scrolling velocity after an + user initiated flick. Please note that this is the easing curve for the positions, \bold{not} + the velocity: the default is QEasingCurve::OutQuad, which results in a linear decrease in + velocity (1st derivative) and a constant deceleration (2nd derivative). + + \value DecelerationFactor This factor influences how long it takes the scroller to decelerate + to 0 velocity. The actual value depends on the chosen ScrollingCurve. For most + types the value should be in the range from \c 0.1 to \c 2.0 + + \value MinimumVelocity The minimum velocity that is needed after ending the touch or releasing + the mouse to start scrolling in \c{m/s}. + + \value MaximumVelocity This is the maximum velocity that can be reached in \c{m/s}. + + \value MaximumClickThroughVelocity This is the maximum allowed scroll speed for a click-through + in \c{m/s}. This means that a click on a currently (slowly) scrolling object will not only stop + the scrolling but the click event will also be delivered to the UI control. This is + useful when using exponential-type scrolling curves. + + \value AcceleratingFlickMaximumTime This is the maximum time in \c seconds that a flick gesture + can take to be recognized as an accelerating flick. If set to zero no such gesture is + detected. An "accelerating flick" is a flick gesture executed on an already scrolling object. + In such cases the scrolling speed is multiplied by AcceleratingFlickSpeedupFactor in order to + accelerate it. + + \value AcceleratingFlickSpeedupFactor The current speed is multiplied by this number if an + accelerating flick is detected. Should be \c{>= 1}. + + \value SnapPositionRatio This is the distance that the user must drag the area beween two snap + points in order to snap it to the next position. \c{0.33} means that the scroll must only + reach one third of the distance between two snap points to snap to the next one. The ratio must + be between \c 0 and \c 1. + + \value SnapTime This is the time factor for the scrolling curve. A lower value means that the + scrolling will take longer. The scrolling distance is independet of this value. + + \value OvershootDragResistanceFactor This value is the factor between the mouse dragging and + the actual scroll area movement (during overshoot). The factor must be between \c 0 and \c 1. + + \value OvershootDragDistanceFactor This is the maximum distance for overshoot movements while + dragging. The actual overshoot distance is calculated by multiplying this value with the + viewport size of the scrolled object. The factor must be between \c 0 and \c 1. + + \value OvershootScrollDistanceFactor This is the maximum distance for overshoot movements while + scrolling. The actual overshoot distance is calculated by multiplying this value with the + viewport size of the scrolled object. The factor must be between \c 0 and \c 1. + + \value OvershootScrollTime This is the time in \c seconds that is used to play the + complete overshoot animation. + + \value HorizontalOvershootPolicy This is the horizontal overshooting policy (see OvershootPolicy). + + \value VerticalOvershootPolicy This is the horizontal overshooting policy (see OvershootPolicy). + + \value FrameRate This is the frame rate which should be used while dragging or scrolling. + QScroller uses a QAbstractAnimation timer internally to sync all scrolling operations to other + animations that might be active at the same time. If the standard value of 60 frames per + second is too fast, it can be lowered with this setting, + while still being in-sync with QAbstractAnimation. Please note that only the values of the + FrameRates enum are allowed here. + + \value ScrollMetricCount This is always the last entry. +*/ + +QT_END_NAMESPACE diff --git a/src/widgets/util/qscrollerproperties.h b/src/widgets/util/qscrollerproperties.h new file mode 100644 index 0000000000..75d8932f81 --- /dev/null +++ b/src/widgets/util/qscrollerproperties.h @@ -0,0 +1,140 @@ +/**************************************************************************** +** +** Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies). +** All rights reserved. +** Contact: Nokia Corporation (qt-info@nokia.com) +** +** This file is part of the QtGui module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** No Commercial Usage +** This file contains pre-release code and may not be distributed. +** You may use this file in accordance with the terms and conditions +** contained in the Technology Preview License Agreement accompanying +** this package. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain additional +** rights. These rights are described in the Nokia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** If you have questions regarding the use of this file, please contact +** Nokia at qt-info@nokia.com. +** +** +** +** +** +** +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QSCROLLERPROPERTIES_H +#define QSCROLLERPROPERTIES_H + +#include <QtCore/QScopedPointer> +#include <QtCore/QMetaType> +#include <QtCore/QVariant> + +QT_BEGIN_HEADER + +QT_BEGIN_NAMESPACE + +QT_MODULE(Gui) + +class QScroller; +class QScrollerPrivate; +class QScrollerPropertiesPrivate; + +class Q_GUI_EXPORT QScrollerProperties +{ +public: + QScrollerProperties(); + QScrollerProperties(const QScrollerProperties &sp); + QScrollerProperties &operator=(const QScrollerProperties &sp); + virtual ~QScrollerProperties(); + + bool operator==(const QScrollerProperties &sp) const; + bool operator!=(const QScrollerProperties &sp) const; + + static void setDefaultScrollerProperties(const QScrollerProperties &sp); + static void unsetDefaultScrollerProperties(); + + enum OvershootPolicy + { + OvershootWhenScrollable, + OvershootAlwaysOff, + OvershootAlwaysOn + }; + + enum FrameRates { + Standard, + Fps60, + Fps30, + Fps20 + }; + + enum ScrollMetric + { + MousePressEventDelay, // qreal [s] + DragStartDistance, // qreal [m] + DragVelocitySmoothingFactor, // qreal [0..1/s] (complex calculation involving time) v = v_new* DASF + v_old * (1-DASF) + AxisLockThreshold, // qreal [0..1] atan(|min(dx,dy)|/|max(dx,dy)|) + + ScrollingCurve, // QEasingCurve + DecelerationFactor, // slope of the curve + + MinimumVelocity, // qreal [m/s] + MaximumVelocity, // qreal [m/s] + MaximumClickThroughVelocity, // qreal [m/s] + + AcceleratingFlickMaximumTime, // qreal [s] + AcceleratingFlickSpeedupFactor, // qreal [1..] + + SnapPositionRatio, // qreal [0..1] + SnapTime, // qreal [s] + + OvershootDragResistanceFactor, // qreal [0..1] + OvershootDragDistanceFactor, // qreal [0..1] + OvershootScrollDistanceFactor, // qreal [0..1] + OvershootScrollTime, // qreal [s] + + HorizontalOvershootPolicy, // enum OvershootPolicy + VerticalOvershootPolicy, // enum OvershootPolicy + FrameRate, // enum FrameRates + + ScrollMetricCount + }; + + QVariant scrollMetric(ScrollMetric metric) const; + void setScrollMetric(ScrollMetric metric, const QVariant &value); + +protected: + QScopedPointer<QScrollerPropertiesPrivate> d; + +private: + QScrollerProperties(QScrollerPropertiesPrivate &dd); + + friend class QScrollerPropertiesPrivate; + friend class QScroller; + friend class QScrollerPrivate; +}; + +QT_END_NAMESPACE + +Q_DECLARE_METATYPE(QScrollerProperties::OvershootPolicy) +Q_DECLARE_METATYPE(QScrollerProperties::FrameRates) + +QT_END_HEADER + +#endif // QSCROLLERPROPERTIES_H diff --git a/src/widgets/util/qscrollerproperties_p.h b/src/widgets/util/qscrollerproperties_p.h new file mode 100644 index 0000000000..76d8b0a79b --- /dev/null +++ b/src/widgets/util/qscrollerproperties_p.h @@ -0,0 +1,94 @@ +/**************************************************************************** +** +** Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies). +** All rights reserved. +** Contact: Nokia Corporation (qt-info@nokia.com) +** +** This file is part of the QtGui module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** No Commercial Usage +** This file contains pre-release code and may not be distributed. +** You may use this file in accordance with the terms and conditions +** contained in the Technology Preview License Agreement accompanying +** this package. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain additional +** rights. These rights are described in the Nokia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** If you have questions regarding the use of this file, please contact +** Nokia at qt-info@nokia.com. +** +** +** +** +** +** +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QSCROLLERPROPERTIES_P_H +#define QSCROLLERPROPERTIES_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QPointF> +#include <QEasingCurve> +#include <qscrollerproperties.h> + +QT_BEGIN_NAMESPACE + +class QScrollerPropertiesPrivate +{ +public: + static QScrollerPropertiesPrivate *defaults(); + + bool operator==(const QScrollerPropertiesPrivate &) const; + + qreal mousePressEventDelay; + qreal dragStartDistance; + qreal dragVelocitySmoothingFactor; + qreal axisLockThreshold; + QEasingCurve scrollingCurve; + qreal decelerationFactor; + qreal minimumVelocity; + qreal maximumVelocity; + qreal maximumClickThroughVelocity; + qreal acceleratingFlickMaximumTime; + qreal acceleratingFlickSpeedupFactor; + qreal snapPositionRatio; + qreal snapTime; + qreal overshootDragResistanceFactor; + qreal overshootDragDistanceFactor; + qreal overshootScrollDistanceFactor; + qreal overshootScrollTime; + QScrollerProperties::OvershootPolicy hOvershootPolicy; + QScrollerProperties::OvershootPolicy vOvershootPolicy; + QScrollerProperties::FrameRates frameRate; +}; + +QT_END_NAMESPACE + +#endif // QSCROLLERPROPERTIES_P_H + diff --git a/src/widgets/util/qsystemtrayicon.cpp b/src/widgets/util/qsystemtrayicon.cpp new file mode 100644 index 0000000000..33cc61ee97 --- /dev/null +++ b/src/widgets/util/qsystemtrayicon.cpp @@ -0,0 +1,674 @@ +/**************************************************************************** +** +** Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies). +** All rights reserved. +** Contact: Nokia Corporation (qt-info@nokia.com) +** +** This file is part of the QtGui module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** No Commercial Usage +** This file contains pre-release code and may not be distributed. +** You may use this file in accordance with the terms and conditions +** contained in the Technology Preview License Agreement accompanying +** this package. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain additional +** rights. These rights are described in the Nokia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** If you have questions regarding the use of this file, please contact +** Nokia at qt-info@nokia.com. +** +** +** +** +** +** +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qsystemtrayicon.h" +#include "qsystemtrayicon_p.h" + +#ifndef QT_NO_SYSTEMTRAYICON + +#include "qmenu.h" +#include "qevent.h" +#include "qpoint.h" +#include "qlabel.h" +#include "qpushbutton.h" +#include "qpainterpath.h" +#include "qpainter.h" +#include "qstyle.h" +#include "qgridlayout.h" +#include "qapplication.h" +#include "qdesktopwidget.h" +#include "qbitmap.h" +#include "private/qlabel_p.h" +#include "qapplication.h" + +QT_BEGIN_NAMESPACE + +/*! + \class QSystemTrayIcon + \brief The QSystemTrayIcon class provides an icon for an application in the system tray. + \since 4.2 + \ingroup desktop + + Modern operating systems usually provide a special area on the desktop, + called the \e{system tray} or \e{notification area}, where long-running + applications can display icons and short messages. + + \image system-tray.png The system tray on Windows XP. + + The QSystemTrayIcon class can be used on the following platforms: + + \list + \o All supported versions of Windows. + \o All window managers for X11 that implement the \l{freedesktop.org} system + tray specification, including recent versions of KDE and GNOME. + \o All supported versions of Mac OS X. Note that the Growl + notification system must be installed for + QSystemTrayIcon::showMessage() to display messages. + \endlist + + To check whether a system tray is present on the user's desktop, + call the QSystemTrayIcon::isSystemTrayAvailable() static function. + + To add a system tray entry, create a QSystemTrayIcon object, call setContextMenu() + to provide a context menu for the icon, and call show() to make it visible in the + system tray. Status notification messages ("balloon messages") can be displayed at + any time using showMessage(). + + If the system tray is unavailable when a system tray icon is constructed, but + becomes available later, QSystemTrayIcon will automatically add an entry for the + application in the system tray if the icon is \l visible. + + The activated() signal is emitted when the user activates the icon. + + Only on X11, when a tooltip is requested, the QSystemTrayIcon receives a QHelpEvent + of type QEvent::ToolTip. Additionally, the QSystemTrayIcon receives wheel events of + type QEvent::Wheel. These are not supported on any other platform. + + \sa QDesktopServices, QDesktopWidget, {Desktop Integration}, {System Tray Icon Example} +*/ + +/*! + \enum QSystemTrayIcon::MessageIcon + + This enum describes the icon that is shown when a balloon message is displayed. + + \value NoIcon No icon is shown. + \value Information An information icon is shown. + \value Warning A standard warning icon is shown. + \value Critical A critical warning icon is shown. + + \sa QMessageBox +*/ + +/*! + Constructs a QSystemTrayIcon object with the given \a parent. + + The icon is initially invisible. + + \sa visible +*/ +QSystemTrayIcon::QSystemTrayIcon(QObject *parent) +: QObject(*new QSystemTrayIconPrivate(), parent) +{ +} + +/*! + Constructs a QSystemTrayIcon object with the given \a icon and \a parent. + + The icon is initially invisible. + + \sa visible +*/ +QSystemTrayIcon::QSystemTrayIcon(const QIcon &icon, QObject *parent) +: QObject(*new QSystemTrayIconPrivate(), parent) +{ + setIcon(icon); +} + +/*! + Removes the icon from the system tray and frees all allocated resources. +*/ +QSystemTrayIcon::~QSystemTrayIcon() +{ + Q_D(QSystemTrayIcon); + d->remove_sys(); +} + +#ifndef QT_NO_MENU + +/*! + Sets the specified \a menu to be the context menu for the system tray icon. + + The menu will pop up when the user requests the context menu for the system + tray icon by clicking the mouse button. + + On Mac OS X, this is currenly converted to a NSMenu, so the + aboutToHide() signal is not emitted. + + \note The system tray icon does not take ownership of the menu. You must + ensure that it is deleted at the appropriate time by, for example, creating + the menu with a suitable parent object. +*/ +void QSystemTrayIcon::setContextMenu(QMenu *menu) +{ + Q_D(QSystemTrayIcon); + d->menu = menu; + d->updateMenu_sys(); +} + +/*! + Returns the current context menu for the system tray entry. +*/ +QMenu* QSystemTrayIcon::contextMenu() const +{ + Q_D(const QSystemTrayIcon); + return d->menu; +} + +#endif // QT_NO_MENU + +/*! + \property QSystemTrayIcon::icon + \brief the system tray icon + + On Windows, the system tray icon size is 16x16; on X11, the preferred size is + 22x22. The icon will be scaled to the appropriate size as necessary. +*/ +void QSystemTrayIcon::setIcon(const QIcon &icon) +{ + Q_D(QSystemTrayIcon); + d->icon = icon; + d->updateIcon_sys(); +} + +QIcon QSystemTrayIcon::icon() const +{ + Q_D(const QSystemTrayIcon); + return d->icon; +} + +/*! + \property QSystemTrayIcon::toolTip + \brief the tooltip for the system tray entry + + On some systems, the tooltip's length is limited. The tooltip will be truncated + if necessary. +*/ +void QSystemTrayIcon::setToolTip(const QString &tooltip) +{ + Q_D(QSystemTrayIcon); + d->toolTip = tooltip; + d->updateToolTip_sys(); +} + +QString QSystemTrayIcon::toolTip() const +{ + Q_D(const QSystemTrayIcon); + return d->toolTip; +} + +/*! + \fn void QSystemTrayIcon::show() + + Shows the icon in the system tray. + + \sa hide(), visible +*/ + +/*! + \fn void QSystemTrayIcon::hide() + + Hides the system tray entry. + + \sa show(), visible +*/ + +/*! + \since 4.3 + Returns the geometry of the system tray icon in screen coordinates. + + \sa visible +*/ +QRect QSystemTrayIcon::geometry() const +{ + Q_D(const QSystemTrayIcon); + if (!d->visible) + return QRect(); + return d->geometry_sys(); +} + +/*! + \property QSystemTrayIcon::visible + \brief whether the system tray entry is visible + + Setting this property to true or calling show() makes the system tray icon + visible; setting this property to false or calling hide() hides it. +*/ +void QSystemTrayIcon::setVisible(bool visible) +{ + Q_D(QSystemTrayIcon); + if (visible == d->visible) + return; + if (d->icon.isNull() && visible) + qWarning("QSystemTrayIcon::setVisible: No Icon set"); + d->visible = visible; + if (d->visible) + d->install_sys(); + else + d->remove_sys(); +} + +bool QSystemTrayIcon::isVisible() const +{ + Q_D(const QSystemTrayIcon); + return d->visible; +} + +/*! + \reimp +*/ +bool QSystemTrayIcon::event(QEvent *e) +{ +#if defined(Q_WS_X11) + if (e->type() == QEvent::ToolTip) { + Q_D(QSystemTrayIcon); + return d->sys->deliverToolTipEvent(e); + } +#endif + return QObject::event(e); +} + +/*! + \enum QSystemTrayIcon::ActivationReason + + This enum describes the reason the system tray was activated. + + \value Unknown Unknown reason + \value Context The context menu for the system tray entry was requested + \value DoubleClick The system tray entry was double clicked + \value Trigger The system tray entry was clicked + \value MiddleClick The system tray entry was clicked with the middle mouse button + + \sa activated() +*/ + +/*! + \fn void QSystemTrayIcon::activated(QSystemTrayIcon::ActivationReason reason) + + This signal is emitted when the user activates the system tray icon. \a reason + specifies the reason for activation. QSystemTrayIcon::ActivationReason enumerates + the various reasons. + + \sa QSystemTrayIcon::ActivationReason +*/ + +/*! + \fn void QSystemTrayIcon::messageClicked() + + This signal is emitted when the message displayed using showMessage() + was clicked by the user. + + Currently this signal is not sent on Mac OS X. + + \note We follow Microsoft Windows XP/Vista behavior, so the + signal is also emitted when the user clicks on a tray icon with + a balloon message displayed. + + \sa activated() +*/ + + +/*! + Returns true if the system tray is available; otherwise returns false. + + If the system tray is currently unavailable but becomes available later, + QSystemTrayIcon will automatically add an entry in the system tray if it + is \l visible. +*/ + +bool QSystemTrayIcon::isSystemTrayAvailable() +{ + return QSystemTrayIconPrivate::isSystemTrayAvailable_sys(); +} + +/*! + Returns true if the system tray supports balloon messages; otherwise returns false. + + \sa showMessage() +*/ +bool QSystemTrayIcon::supportsMessages() +{ + return QSystemTrayIconPrivate::supportsMessages_sys(); +} + +/*! + \fn void QSystemTrayIcon::showMessage(const QString &title, const QString &message, MessageIcon icon, int millisecondsTimeoutHint) + \since 4.3 + + Shows a balloon message for the entry with the given \a title, \a message and + \a icon for the time specified in \a millisecondsTimeoutHint. \a title and \a message + must be plain text strings. + + Message can be clicked by the user; the messageClicked() signal will emitted when + this occurs. + + Note that display of messages are dependent on the system configuration and user + preferences, and that messages may not appear at all. Hence, it should not be + relied upon as the sole means for providing critical information. + + On Windows, the \a millisecondsTimeoutHint is usually ignored by the system + when the application has focus. + + On Mac OS X, the Growl notification system must be installed for this function to + display messages. + + \sa show() supportsMessages() + */ +void QSystemTrayIcon::showMessage(const QString& title, const QString& msg, + QSystemTrayIcon::MessageIcon icon, int msecs) +{ + Q_D(QSystemTrayIcon); + if (d->visible) + d->showMessage_sys(title, msg, icon, msecs); +} + +////////////////////////////////////////////////////////////////////// +static QBalloonTip *theSolitaryBalloonTip = 0; + +void QBalloonTip::showBalloon(QSystemTrayIcon::MessageIcon icon, const QString& title, + const QString& message, QSystemTrayIcon *trayIcon, + const QPoint& pos, int timeout, bool showArrow) +{ + hideBalloon(); + if (message.isEmpty() && title.isEmpty()) + return; + + theSolitaryBalloonTip = new QBalloonTip(icon, title, message, trayIcon); + if (timeout < 0) + timeout = 10000; //10 s default + theSolitaryBalloonTip->balloon(pos, timeout, showArrow); +} + +void QBalloonTip::hideBalloon() +{ + if (!theSolitaryBalloonTip) + return; + theSolitaryBalloonTip->hide(); + delete theSolitaryBalloonTip; + theSolitaryBalloonTip = 0; +} + +bool QBalloonTip::isBalloonVisible() +{ + return theSolitaryBalloonTip; +} + +QBalloonTip::QBalloonTip(QSystemTrayIcon::MessageIcon icon, const QString& title, + const QString& message, QSystemTrayIcon *ti) + : QWidget(0, Qt::ToolTip), trayIcon(ti), timerId(-1) +{ + setAttribute(Qt::WA_DeleteOnClose); + QObject::connect(ti, SIGNAL(destroyed()), this, SLOT(close())); + + QLabel *titleLabel = new QLabel; + titleLabel->installEventFilter(this); + titleLabel->setText(title); + QFont f = titleLabel->font(); + f.setBold(true); +#ifdef Q_WS_WINCE + f.setPointSize(f.pointSize() - 2); +#endif + titleLabel->setFont(f); + titleLabel->setTextFormat(Qt::PlainText); // to maintain compat with windows + +#ifdef Q_WS_WINCE + const int iconSize = style()->pixelMetric(QStyle::PM_SmallIconSize); + const int closeButtonSize = style()->pixelMetric(QStyle::PM_SmallIconSize) - 2; +#else + const int iconSize = 18; + const int closeButtonSize = 15; +#endif + + QPushButton *closeButton = new QPushButton; + closeButton->setIcon(style()->standardIcon(QStyle::SP_TitleBarCloseButton)); + closeButton->setIconSize(QSize(closeButtonSize, closeButtonSize)); + closeButton->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); + closeButton->setFixedSize(closeButtonSize, closeButtonSize); + QObject::connect(closeButton, SIGNAL(clicked()), this, SLOT(close())); + + QLabel *msgLabel = new QLabel; +#ifdef Q_WS_WINCE + f.setBold(false); + msgLabel->setFont(f); +#endif + msgLabel->installEventFilter(this); + msgLabel->setText(message); + msgLabel->setTextFormat(Qt::PlainText); + msgLabel->setAlignment(Qt::AlignTop | Qt::AlignLeft); + + // smart size for the message label +#ifdef Q_WS_WINCE + int limit = QApplication::desktop()->availableGeometry(msgLabel).size().width() / 2; +#else + int limit = QApplication::desktop()->availableGeometry(msgLabel).size().width() / 3; +#endif + if (msgLabel->sizeHint().width() > limit) { + msgLabel->setWordWrap(true); + if (msgLabel->sizeHint().width() > limit) { + msgLabel->d_func()->ensureTextControl(); + if (QTextControl *control = msgLabel->d_func()->control) { + QTextOption opt = control->document()->defaultTextOption(); + opt.setWrapMode(QTextOption::WrapAnywhere); + control->document()->setDefaultTextOption(opt); + } + } +#ifdef Q_WS_WINCE + // Make sure that the text isn't wrapped "somewhere" in the balloon widget + // in the case that we have a long title label. + setMaximumWidth(limit); +#else + // Here we allow the text being much smaller than the balloon widget + // to emulate the weird standard windows behavior. + msgLabel->setFixedSize(limit, msgLabel->heightForWidth(limit)); +#endif + } + + QIcon si; + switch (icon) { + case QSystemTrayIcon::Warning: + si = style()->standardIcon(QStyle::SP_MessageBoxWarning); + break; + case QSystemTrayIcon::Critical: + si = style()->standardIcon(QStyle::SP_MessageBoxCritical); + break; + case QSystemTrayIcon::Information: + si = style()->standardIcon(QStyle::SP_MessageBoxInformation); + break; + case QSystemTrayIcon::NoIcon: + default: + break; + } + + QGridLayout *layout = new QGridLayout; + if (!si.isNull()) { + QLabel *iconLabel = new QLabel; + iconLabel->setPixmap(si.pixmap(iconSize, iconSize)); + iconLabel->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); + iconLabel->setMargin(2); + layout->addWidget(iconLabel, 0, 0); + layout->addWidget(titleLabel, 0, 1); + } else { + layout->addWidget(titleLabel, 0, 0, 1, 2); + } + + layout->addWidget(closeButton, 0, 2); + layout->addWidget(msgLabel, 1, 0, 1, 3); + layout->setSizeConstraint(QLayout::SetFixedSize); + layout->setMargin(3); + setLayout(layout); + + QPalette pal = palette(); + pal.setColor(QPalette::Window, QColor(0xff, 0xff, 0xe1)); + pal.setColor(QPalette::WindowText, Qt::black); + setPalette(pal); +} + +QBalloonTip::~QBalloonTip() +{ + theSolitaryBalloonTip = 0; +} + +void QBalloonTip::paintEvent(QPaintEvent *) +{ + QPainter painter(this); + painter.drawPixmap(rect(), pixmap); +} + +void QBalloonTip::resizeEvent(QResizeEvent *ev) +{ + QWidget::resizeEvent(ev); +} + +void QBalloonTip::balloon(const QPoint& pos, int msecs, bool showArrow) +{ + QRect scr = QApplication::desktop()->screenGeometry(pos); + QSize sh = sizeHint(); + const int border = 1; + const int ah = 18, ao = 18, aw = 18, rc = 7; + bool arrowAtTop = (pos.y() + sh.height() + ah < scr.height()); + bool arrowAtLeft = (pos.x() + sh.width() - ao < scr.width()); + setContentsMargins(border + 3, border + (arrowAtTop ? ah : 0) + 2, border + 3, border + (arrowAtTop ? 0 : ah) + 2); + updateGeometry(); + sh = sizeHint(); + + int ml, mr, mt, mb; + QSize sz = sizeHint(); + if (!arrowAtTop) { + ml = mt = 0; + mr = sz.width() - 1; + mb = sz.height() - ah - 1; + } else { + ml = 0; + mt = ah; + mr = sz.width() - 1; + mb = sz.height() - 1; + } + + QPainterPath path; +#if defined(QT_NO_XSHAPE) && defined(Q_WS_X11) + // XShape is required for setting the mask, so we just + // draw an ugly square when its not available + path.moveTo(0, 0); + path.lineTo(sz.width() - 1, 0); + path.lineTo(sz.width() - 1, sz.height() - 1); + path.lineTo(0, sz.height() - 1); + path.lineTo(0, 0); + move(qMax(pos.x() - sz.width(), scr.left()), pos.y()); +#else + path.moveTo(ml + rc, mt); + if (arrowAtTop && arrowAtLeft) { + if (showArrow) { + path.lineTo(ml + ao, mt); + path.lineTo(ml + ao, mt - ah); + path.lineTo(ml + ao + aw, mt); + } + move(qMax(pos.x() - ao, scr.left() + 2), pos.y()); + } else if (arrowAtTop && !arrowAtLeft) { + if (showArrow) { + path.lineTo(mr - ao - aw, mt); + path.lineTo(mr - ao, mt - ah); + path.lineTo(mr - ao, mt); + } + move(qMin(pos.x() - sh.width() + ao, scr.right() - sh.width() - 2), pos.y()); + } + path.lineTo(mr - rc, mt); + path.arcTo(QRect(mr - rc*2, mt, rc*2, rc*2), 90, -90); + path.lineTo(mr, mb - rc); + path.arcTo(QRect(mr - rc*2, mb - rc*2, rc*2, rc*2), 0, -90); + if (!arrowAtTop && !arrowAtLeft) { + if (showArrow) { + path.lineTo(mr - ao, mb); + path.lineTo(mr - ao, mb + ah); + path.lineTo(mr - ao - aw, mb); + } + move(qMin(pos.x() - sh.width() + ao, scr.right() - sh.width() - 2), + pos.y() - sh.height()); + } else if (!arrowAtTop && arrowAtLeft) { + if (showArrow) { + path.lineTo(ao + aw, mb); + path.lineTo(ao, mb + ah); + path.lineTo(ao, mb); + } + move(qMax(pos.x() - ao, scr.x() + 2), pos.y() - sh.height()); + } + path.lineTo(ml + rc, mb); + path.arcTo(QRect(ml, mb - rc*2, rc*2, rc*2), -90, -90); + path.lineTo(ml, mt + rc); + path.arcTo(QRect(ml, mt, rc*2, rc*2), 180, -90); + + // Set the mask + QBitmap bitmap = QBitmap(sizeHint()); + bitmap.fill(Qt::color0); + QPainter painter1(&bitmap); + painter1.setPen(QPen(Qt::color1, border)); + painter1.setBrush(QBrush(Qt::color1)); + painter1.drawPath(path); + setMask(bitmap); +#endif + + // Draw the border + pixmap = QPixmap(sz); + QPainter painter2(&pixmap); + painter2.setPen(QPen(palette().color(QPalette::Window).darker(160), border)); + painter2.setBrush(palette().color(QPalette::Window)); + painter2.drawPath(path); + + if (msecs > 0) + timerId = startTimer(msecs); + show(); +} + +void QBalloonTip::mousePressEvent(QMouseEvent *e) +{ + close(); + if(e->button() == Qt::LeftButton) + emit trayIcon->messageClicked(); +} + +void QBalloonTip::timerEvent(QTimerEvent *e) +{ + if (e->timerId() == timerId) { + killTimer(timerId); + if (!underMouse()) + close(); + return; + } + QWidget::timerEvent(e); +} + +void qtsystray_sendActivated(QSystemTrayIcon *i, int r) +{ + emit i->activated((QSystemTrayIcon::ActivationReason)r); +} + +QT_END_NAMESPACE + +#endif // QT_NO_SYSTEMTRAYICON diff --git a/src/widgets/util/qsystemtrayicon.h b/src/widgets/util/qsystemtrayicon.h new file mode 100644 index 0000000000..1f0dab5393 --- /dev/null +++ b/src/widgets/util/qsystemtrayicon.h @@ -0,0 +1,132 @@ +/**************************************************************************** +** +** Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies). +** All rights reserved. +** Contact: Nokia Corporation (qt-info@nokia.com) +** +** This file is part of the QtGui module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** No Commercial Usage +** This file contains pre-release code and may not be distributed. +** You may use this file in accordance with the terms and conditions +** contained in the Technology Preview License Agreement accompanying +** this package. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain additional +** rights. These rights are described in the Nokia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** If you have questions regarding the use of this file, please contact +** Nokia at qt-info@nokia.com. +** +** +** +** +** +** +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QSYSTEMTRAYICON_H +#define QSYSTEMTRAYICON_H + +#include <QtCore/qobject.h> + +#ifndef QT_NO_SYSTEMTRAYICON + +#include <QtGui/qicon.h> + +QT_BEGIN_HEADER + +QT_BEGIN_NAMESPACE + +QT_MODULE(Gui) + +class QSystemTrayIconPrivate; + +class QMenu; +class QEvent; +class QWheelEvent; +class QMouseEvent; +class QPoint; + +class Q_GUI_EXPORT QSystemTrayIcon : public QObject +{ + Q_OBJECT + Q_PROPERTY(QString toolTip READ toolTip WRITE setToolTip) + Q_PROPERTY(QIcon icon READ icon WRITE setIcon) + Q_PROPERTY(bool visible READ isVisible WRITE setVisible DESIGNABLE false) + +public: + QSystemTrayIcon(QObject *parent = 0); + QSystemTrayIcon(const QIcon &icon, QObject *parent = 0); + ~QSystemTrayIcon(); + + enum ActivationReason { + Unknown, + Context, + DoubleClick, + Trigger, + MiddleClick + }; + +#ifndef QT_NO_MENU + void setContextMenu(QMenu *menu); + QMenu *contextMenu() const; +#endif + + QIcon icon() const; + void setIcon(const QIcon &icon); + + QString toolTip() const; + void setToolTip(const QString &tip); + + static bool isSystemTrayAvailable(); + static bool supportsMessages(); + + enum MessageIcon { NoIcon, Information, Warning, Critical }; + void showMessage(const QString &title, const QString &msg, + MessageIcon icon = Information, int msecs = 10000); + + QRect geometry() const; + bool isVisible() const; + +public Q_SLOTS: + void setVisible(bool visible); + inline void show() { setVisible(true); } + inline void hide() { setVisible(false); } + +Q_SIGNALS: + void activated(QSystemTrayIcon::ActivationReason reason); + void messageClicked(); + +protected: + bool event(QEvent *event); + +private: + Q_DISABLE_COPY(QSystemTrayIcon) + Q_DECLARE_PRIVATE(QSystemTrayIcon) + + friend class QSystemTrayIconSys; + friend class QBalloonTip; + friend void qtsystray_sendActivated(QSystemTrayIcon *, int); +}; + +QT_END_NAMESPACE + +QT_END_HEADER + +#endif // QT_NO_SYSTEMTRAYICON +#endif // QSYSTEMTRAYICON_H diff --git a/src/widgets/util/qsystemtrayicon_mac.mm b/src/widgets/util/qsystemtrayicon_mac.mm new file mode 100644 index 0000000000..0ec0b07040 --- /dev/null +++ b/src/widgets/util/qsystemtrayicon_mac.mm @@ -0,0 +1,578 @@ +/**************************************************************************** +** +** Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies). +** All rights reserved. +** Contact: Nokia Corporation (qt-info@nokia.com) +** +** This file is part of the QtGui module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** No Commercial Usage +** This file contains pre-release code and may not be distributed. +** You may use this file in accordance with the terms and conditions +** contained in the Technology Preview License Agreement accompanying +** this package. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain additional +** rights. These rights are described in the Nokia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** If you have questions regarding the use of this file, please contact +** Nokia at qt-info@nokia.com. +** +** +** +** +** +** +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +/**************************************************************************** +** +** Copyright (c) 2007-2008, Apple, Inc. +** +** All rights reserved. +** +** Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are met: +** +** * Redistributions of source code must retain the above copyright notice, +** this list of conditions and the following disclaimer. +** +** * Redistributions in binary form must reproduce the above copyright notice, +** this list of conditions and the following disclaimer in the documentation +** and/or other materials provided with the distribution. +** +** * Neither the name of Apple, Inc. nor the names of its contributors +** may be used to endorse or promote products derived from this software +** without specific prior written permission. +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +** CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +** EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +** PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +** PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +** LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +** NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +** SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +** +****************************************************************************/ + +#define QT_MAC_SYSTEMTRAY_USE_GROWL + +#include <private/qt_cocoa_helpers_mac_p.h> +#include <private/qsystemtrayicon_p.h> +#include <qtemporaryfile.h> +#include <qimagewriter.h> +#include <qapplication.h> +#include <qdebug.h> +#include <qstyle.h> + +#include <private/qt_mac_p.h> +#import <AppKit/AppKit.h> + +QT_BEGIN_NAMESPACE +extern bool qt_mac_execute_apple_script(const QString &script, AEDesc *ret); //qapplication_mac.cpp +extern void qtsystray_sendActivated(QSystemTrayIcon *i, int r); //qsystemtrayicon.cpp +extern NSString *keySequenceToKeyEqivalent(const QKeySequence &accel); // qmenu_mac.mm +extern NSUInteger keySequenceModifierMask(const QKeySequence &accel); // qmenu_mac.mm +extern Qt::MouseButton cocoaButton2QtButton(NSInteger buttonNum); +QT_END_NAMESPACE + +QT_USE_NAMESPACE + +@class QT_MANGLE_NAMESPACE(QNSMenu); +@class QT_MANGLE_NAMESPACE(QNSImageView); + +@interface QT_MANGLE_NAMESPACE(QNSStatusItem) : NSObject { + NSStatusItem *item; + QSystemTrayIcon *icon; + QSystemTrayIconPrivate *iconPrivate; + QT_MANGLE_NAMESPACE(QNSImageView) *imageCell; +} +-(id)initWithIcon:(QSystemTrayIcon*)icon iconPrivate:(QSystemTrayIconPrivate *)iprivate; +-(void)dealloc; +-(QSystemTrayIcon*)icon; +-(NSStatusItem*)item; +-(QRectF)geometry; +- (void)triggerSelector:(id)sender button:(Qt::MouseButton)mouseButton; +- (void)doubleClickSelector:(id)sender; +@end + +@interface QT_MANGLE_NAMESPACE(QNSImageView) : NSImageView { + BOOL down; + QT_MANGLE_NAMESPACE(QNSStatusItem) *parent; +} +-(id)initWithParent:(QT_MANGLE_NAMESPACE(QNSStatusItem)*)myParent; +-(QSystemTrayIcon*)icon; +-(void)menuTrackingDone:(NSNotification*)notification; +-(void)mousePressed:(NSEvent *)mouseEvent button:(Qt::MouseButton)mouseButton; +@end + + +#if MAC_OS_X_VERSION_MAX_ALLOWED <= MAC_OS_X_VERSION_10_5 + +@protocol NSMenuDelegate <NSObject> +-(void)menuNeedsUpdate:(NSMenu*)menu; +@end +#endif + + +@interface QT_MANGLE_NAMESPACE(QNSMenu) : NSMenu <NSMenuDelegate> { + QMenu *qmenu; +} +-(QMenu*)menu; +-(id)initWithQMenu:(QMenu*)qmenu; +-(void)selectedAction:(id)item; +@end + +QT_BEGIN_NAMESPACE +class QSystemTrayIconSys +{ +public: + QSystemTrayIconSys(QSystemTrayIcon *icon, QSystemTrayIconPrivate *d) { + QMacCocoaAutoReleasePool pool; + item = [[QT_MANGLE_NAMESPACE(QNSStatusItem) alloc] initWithIcon:icon iconPrivate:d]; + } + ~QSystemTrayIconSys() { + QMacCocoaAutoReleasePool pool; + [[[item item] view] setHidden: YES]; + [item release]; + } + QT_MANGLE_NAMESPACE(QNSStatusItem) *item; +}; + +void QSystemTrayIconPrivate::install_sys() +{ + Q_Q(QSystemTrayIcon); + if (!sys) { + sys = new QSystemTrayIconSys(q, this); + updateIcon_sys(); + updateMenu_sys(); + updateToolTip_sys(); + } +} + +QRect QSystemTrayIconPrivate::geometry_sys() const +{ + if(sys) { + const QRectF geom = [sys->item geometry]; + if(!geom.isNull()) + return geom.toRect(); + } + return QRect(); +} + +void QSystemTrayIconPrivate::remove_sys() +{ + delete sys; + sys = 0; +} + +void QSystemTrayIconPrivate::updateIcon_sys() +{ + if(sys && !icon.isNull()) { + QMacCocoaAutoReleasePool pool; +#ifndef QT_MAC_USE_COCOA + const short scale = GetMBarHeight()-4; +#else + CGFloat hgt = [[[NSApplication sharedApplication] mainMenu] menuBarHeight]; + const short scale = hgt - 4; +#endif + NSImage *nsimage = static_cast<NSImage *>(qt_mac_create_nsimage(icon.pixmap(QSize(scale, scale)))); + [(NSImageView*)[[sys->item item] view] setImage: nsimage]; + [nsimage release]; + } +} + +void QSystemTrayIconPrivate::updateMenu_sys() +{ + if(sys) { + QMacCocoaAutoReleasePool pool; + if(menu && !menu->isEmpty()) { + [[sys->item item] setHighlightMode:YES]; + } else { + [[sys->item item] setHighlightMode:NO]; + } + } +} + +void QSystemTrayIconPrivate::updateToolTip_sys() +{ + if(sys) { + QMacCocoaAutoReleasePool pool; + QCFString string(toolTip); + [[[sys->item item] view] setToolTip:(NSString*)static_cast<CFStringRef>(string)]; + } +} + +bool QSystemTrayIconPrivate::isSystemTrayAvailable_sys() +{ + return true; +} + +bool QSystemTrayIconPrivate::supportsMessages_sys() +{ + return true; +} + +void QSystemTrayIconPrivate::showMessage_sys(const QString &title, const QString &message, QSystemTrayIcon::MessageIcon icon, int) +{ + + if(sys) { +#ifdef QT_MAC_SYSTEMTRAY_USE_GROWL + // Make sure that we have Growl installed on the machine we are running on. + QCFType<CFURLRef> cfurl; + OSStatus status = LSGetApplicationForInfo(kLSUnknownType, kLSUnknownCreator, + CFSTR("growlTicket"), kLSRolesAll, 0, &cfurl); + if (status == kLSApplicationNotFoundErr) + return; + QCFType<CFBundleRef> bundle = CFBundleCreate(0, cfurl); + + if (CFStringCompare(CFBundleGetIdentifier(bundle), CFSTR("com.Growl.GrowlHelperApp"), + kCFCompareCaseInsensitive | kCFCompareBackwards) != kCFCompareEqualTo) + return; + QPixmap notificationIconPixmap; + if(icon == QSystemTrayIcon::Information) + notificationIconPixmap = QApplication::style()->standardPixmap(QStyle::SP_MessageBoxInformation); + else if(icon == QSystemTrayIcon::Warning) + notificationIconPixmap = QApplication::style()->standardPixmap(QStyle::SP_MessageBoxWarning); + else if(icon == QSystemTrayIcon::Critical) + notificationIconPixmap = QApplication::style()->standardPixmap(QStyle::SP_MessageBoxCritical); + QTemporaryFile notificationIconFile; + QString notificationType(QLatin1String("Notification")), notificationIcon, notificationApp(QApplication::applicationName()); + if(notificationApp.isEmpty()) + notificationApp = QLatin1String("Application"); + if(!notificationIconPixmap.isNull() && notificationIconFile.open()) { + QImageWriter writer(¬ificationIconFile, "PNG"); + if(writer.write(notificationIconPixmap.toImage())) + notificationIcon = QLatin1String("image from location \"file://") + notificationIconFile.fileName() + QLatin1String("\""); + } + const QString script(QLatin1String( + "tell application \"GrowlHelperApp\"\n" + "-- Make a list of all the notification types (all)\n" + "set the allNotificationsList to {\"") + notificationType + QLatin1String("\"}\n" + + "-- Make a list of the notifications (enabled)\n" + "set the enabledNotificationsList to {\"") + notificationType + QLatin1String("\"}\n" + + "-- Register our script with growl.\n" + "register as application \"") + notificationApp + QLatin1String("\" all notifications allNotificationsList default notifications enabledNotificationsList\n" + + "-- Send a Notification...\n") + + QLatin1String("notify with name \"") + notificationType + + QLatin1String("\" title \"") + title + + QLatin1String("\" description \"") + message + + QLatin1String("\" application name \"") + notificationApp + + QLatin1String("\" ") + notificationIcon + + QLatin1String("\nend tell")); + qt_mac_execute_apple_script(script, 0); +#elif 0 + Q_Q(QSystemTrayIcon); + NSView *v = [[sys->item item] view]; + NSWindow *w = [v window]; + w = [[sys->item item] window]; + qDebug() << w << v; + QPoint p(qRound([w frame].origin.x), qRound([w frame].origin.y)); + qDebug() << p; + QBalloonTip::showBalloon(icon, message, title, q, QPoint(0, 0), msecs); +#else + Q_UNUSED(icon); + Q_UNUSED(title); + Q_UNUSED(message); +#endif + } +} +QT_END_NAMESPACE + +@implementation NSStatusItem (Qt) +@end + +@implementation QT_MANGLE_NAMESPACE(QNSImageView) +-(id)initWithParent:(QT_MANGLE_NAMESPACE(QNSStatusItem)*)myParent { + self = [super init]; + parent = myParent; + down = NO; + return self; +} + +-(QSystemTrayIcon*)icon { + return [parent icon]; +} + +-(void)menuTrackingDone:(NSNotification*)notification +{ + Q_UNUSED(notification); + down = NO; + + if( ![self icon]->icon().isNull() ) { +#ifndef QT_MAC_USE_COCOA + const short scale = GetMBarHeight()-4; +#else + CGFloat hgt = [[[NSApplication sharedApplication] mainMenu] menuBarHeight]; + const short scale = hgt - 4; +#endif + NSImage *nsimage = static_cast<NSImage *>(qt_mac_create_nsimage([self icon]->icon().pixmap(QSize(scale, scale)))); + [self setImage: nsimage]; + [nsimage release]; + } + + if([self icon]->contextMenu()) + [self icon]->contextMenu()->hide(); + + [self setNeedsDisplay:YES]; +} + +-(void)mousePressed:(NSEvent *)mouseEvent button:(Qt::MouseButton)mouseButton +{ + down = YES; + int clickCount = [mouseEvent clickCount]; + [self setNeedsDisplay:YES]; + +#ifndef QT_MAC_USE_COCOA + const short scale = GetMBarHeight()-4; +#else + CGFloat hgt = [[[NSApplication sharedApplication] mainMenu] menuBarHeight]; + const short scale = hgt - 4; +#endif + + if (![self icon]->icon().isNull() ) { + NSImage *nsaltimage = static_cast<NSImage *>(qt_mac_create_nsimage([self icon]->icon().pixmap(QSize(scale, scale), QIcon::Selected))); + [self setImage: nsaltimage]; + [nsaltimage release]; + } + + if ((clickCount == 2)) { + [self menuTrackingDone:nil]; + [parent doubleClickSelector:self]; + } else { + [parent triggerSelector:self button:mouseButton]; + } +} + +-(void)mouseDown:(NSEvent *)mouseEvent +{ + [self mousePressed:mouseEvent button:Qt::LeftButton]; +} + +-(void)mouseUp:(NSEvent *)mouseEvent +{ + Q_UNUSED(mouseEvent); + [self menuTrackingDone:nil]; +} + +- (void)rightMouseDown:(NSEvent *)mouseEvent +{ + [self mousePressed:mouseEvent button:Qt::RightButton]; +} + +-(void)rightMouseUp:(NSEvent *)mouseEvent +{ + Q_UNUSED(mouseEvent); + [self menuTrackingDone:nil]; +} + +- (void)otherMouseDown:(NSEvent *)mouseEvent +{ + [self mousePressed:mouseEvent button:cocoaButton2QtButton([mouseEvent buttonNumber])]; +} + +-(void)otherMouseUp:(NSEvent *)mouseEvent +{ + Q_UNUSED(mouseEvent); + [self menuTrackingDone:nil]; +} + +-(void)drawRect:(NSRect)rect { + [[parent item] drawStatusBarBackgroundInRect:rect withHighlight:down]; + [super drawRect:rect]; +} +@end + +@implementation QT_MANGLE_NAMESPACE(QNSStatusItem) + +-(id)initWithIcon:(QSystemTrayIcon*)i iconPrivate:(QSystemTrayIconPrivate *)iPrivate +{ + self = [super init]; + if(self) { + icon = i; + iconPrivate = iPrivate; + item = [[[NSStatusBar systemStatusBar] statusItemWithLength:NSSquareStatusItemLength] retain]; + imageCell = [[QT_MANGLE_NAMESPACE(QNSImageView) alloc] initWithParent:self]; + [item setView: imageCell]; + } + return self; +} +-(void)dealloc { + [[NSStatusBar systemStatusBar] removeStatusItem:item]; + [imageCell release]; + [item release]; + [super dealloc]; + +} + +-(QSystemTrayIcon*)icon { + return icon; +} + +-(NSStatusItem*)item { + return item; +} +-(QRectF)geometry { + if(NSWindow *window = [[item view] window]) { + NSRect screenRect = [[window screen] frame]; + NSRect windowRect = [window frame]; + return QRectF(windowRect.origin.x, screenRect.size.height-windowRect.origin.y-windowRect.size.height, windowRect.size.width, windowRect.size.height); + } + return QRectF(); +} + +- (void)triggerSelector:(id)sender button:(Qt::MouseButton)mouseButton { + Q_UNUSED(sender); + if (!icon) + return; + + if (mouseButton == Qt::MidButton) + qtsystray_sendActivated(icon, QSystemTrayIcon::MiddleClick); + else + qtsystray_sendActivated(icon, QSystemTrayIcon::Trigger); + + if (icon->contextMenu()) { +#ifndef QT_MAC_USE_COCOA + [[[self item] view] removeAllToolTips]; + iconPrivate->updateToolTip_sys(); +#endif + NSMenu *m = [[QT_MANGLE_NAMESPACE(QNSMenu) alloc] initWithQMenu:icon->contextMenu()]; + [m setAutoenablesItems: NO]; + [[NSNotificationCenter defaultCenter] addObserver:imageCell + selector:@selector(menuTrackingDone:) + name:NSMenuDidEndTrackingNotification + object:m]; + [item popUpStatusItemMenu: m]; + [m release]; + } +} + +- (void)doubleClickSelector:(id)sender { + Q_UNUSED(sender); + if(!icon) + return; + qtsystray_sendActivated(icon, QSystemTrayIcon::DoubleClick); +} + +@end + +class QSystemTrayIconQMenu : public QMenu +{ +public: + void doAboutToShow() { emit aboutToShow(); } +private: + QSystemTrayIconQMenu(); +}; + +@implementation QT_MANGLE_NAMESPACE(QNSMenu) +-(id)initWithQMenu:(QMenu*)qm { + self = [super init]; + if(self) { + self->qmenu = qm; + [self setDelegate:self]; + } + return self; +} +-(QMenu*)menu { + return qmenu; +} +-(void)menuNeedsUpdate:(NSMenu*)nsmenu { + QT_MANGLE_NAMESPACE(QNSMenu) *menu = static_cast<QT_MANGLE_NAMESPACE(QNSMenu) *>(nsmenu); + emit static_cast<QSystemTrayIconQMenu*>(menu->qmenu)->doAboutToShow(); + for(int i = [menu numberOfItems]-1; i >= 0; --i) + [menu removeItemAtIndex:i]; + QList<QAction*> actions = menu->qmenu->actions();; + for(int i = 0; i < actions.size(); ++i) { + const QAction *action = actions[i]; + if(!action->isVisible()) + continue; + + NSMenuItem *item = 0; + bool needRelease = false; + if(action->isSeparator()) { + item = [NSMenuItem separatorItem]; + } else { + item = [[NSMenuItem alloc] init]; + needRelease = true; + QString text = action->text(); + QKeySequence accel = action->shortcut(); + { + int st = text.lastIndexOf(QLatin1Char('\t')); + if(st != -1) { + accel = QKeySequence(text.right(text.length()-(st+1))); + text.remove(st, text.length()-st); + } + } + if(accel.count() > 1) + text += QLatin1String(" (****)"); //just to denote a multi stroke shortcut + + [item setTitle:(NSString*)QCFString::toCFStringRef(qt_mac_removeMnemonics(text))]; + [item setEnabled:menu->qmenu->isEnabled() && action->isEnabled()]; + [item setState:action->isChecked() ? NSOnState : NSOffState]; + [item setToolTip:(NSString*)QCFString::toCFStringRef(action->toolTip())]; + const QIcon icon = action->icon(); + if(!icon.isNull()) { +#ifndef QT_MAC_USE_COCOA + const short scale = GetMBarHeight(); +#else + const short scale = [[NSApp mainMenu] menuBarHeight]; +#endif + NSImage *nsimage = static_cast<NSImage *>(qt_mac_create_nsimage(icon.pixmap(QSize(scale, scale)))); + [item setImage: nsimage]; + [nsimage release]; + } + if(action->menu()) { + QT_MANGLE_NAMESPACE(QNSMenu) *sub = [[QT_MANGLE_NAMESPACE(QNSMenu) alloc] initWithQMenu:action->menu()]; + [item setSubmenu:sub]; + } else { + [item setAction:@selector(selectedAction:)]; + [item setTarget:self]; + } + if(!accel.isEmpty()) { + [item setKeyEquivalent:keySequenceToKeyEqivalent(accel)]; + [item setKeyEquivalentModifierMask:keySequenceModifierMask(accel)]; + } + } + if(item) + [menu addItem:item]; + if (needRelease) + [item release]; + } +} +-(void)selectedAction:(id)a { + const int activated = [self indexOfItem:a]; + QAction *action = 0; + QList<QAction*> actions = qmenu->actions(); + for(int i = 0, cnt = 0; i < actions.size(); ++i) { + if(actions.at(i)->isVisible() && (cnt++) == activated) { + action = actions.at(i); + break; + } + } + if(action) { + action->activate(QAction::Trigger); + } +} +@end + diff --git a/src/widgets/util/qsystemtrayicon_p.h b/src/widgets/util/qsystemtrayicon_p.h new file mode 100644 index 0000000000..cc7bd47092 --- /dev/null +++ b/src/widgets/util/qsystemtrayicon_p.h @@ -0,0 +1,186 @@ +/**************************************************************************** +** +** Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies). +** All rights reserved. +** Contact: Nokia Corporation (qt-info@nokia.com) +** +** This file is part of the QtGui module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** No Commercial Usage +** This file contains pre-release code and may not be distributed. +** You may use this file in accordance with the terms and conditions +** contained in the Technology Preview License Agreement accompanying +** this package. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain additional +** rights. These rights are described in the Nokia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** If you have questions regarding the use of this file, please contact +** Nokia at qt-info@nokia.com. +** +** +** +** +** +** +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QSYSTEMTRAYICON_P_H +#define QSYSTEMTRAYICON_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists for the convenience +// of a number of Qt sources files. This header file may change from +// version to version without notice, or even be removed. +// +// We mean it. +// + +#include "qsystemtrayicon.h" +#include "private/qobject_p.h" + +#ifndef QT_NO_SYSTEMTRAYICON + +#include "QtGui/qmenu.h" +#include "QtGui/qpixmap.h" +#include "QtCore/qstring.h" +#include "QtCore/qpointer.h" + +QT_BEGIN_NAMESPACE + +class QSystemTrayIconSys; +class QToolButton; +class QLabel; + +class QSystemTrayIconPrivate : public QObjectPrivate +{ + Q_DECLARE_PUBLIC(QSystemTrayIcon) + +public: + QSystemTrayIconPrivate() : sys(0), visible(false) { } + + void install_sys(); + void remove_sys(); + void updateIcon_sys(); + void updateToolTip_sys(); + void updateMenu_sys(); + QRect geometry_sys() const; + void showMessage_sys(const QString &msg, const QString &title, QSystemTrayIcon::MessageIcon icon, int secs); + + static bool isSystemTrayAvailable_sys(); + static bool supportsMessages_sys(); + + QPointer<QMenu> menu; + QIcon icon; + QString toolTip; + QSystemTrayIconSys *sys; + bool visible; +}; + +class QBalloonTip : public QWidget +{ + Q_OBJECT +public: + static void showBalloon(QSystemTrayIcon::MessageIcon icon, const QString& title, + const QString& msg, QSystemTrayIcon *trayIcon, + const QPoint& pos, int timeout, bool showArrow = true); + static void hideBalloon(); + static bool isBalloonVisible(); + +private: + QBalloonTip(QSystemTrayIcon::MessageIcon icon, const QString& title, + const QString& msg, QSystemTrayIcon *trayIcon); + ~QBalloonTip(); + void balloon(const QPoint&, int, bool); + +protected: + void paintEvent(QPaintEvent *); + void resizeEvent(QResizeEvent *); + void mousePressEvent(QMouseEvent *e); + void timerEvent(QTimerEvent *e); + +private: + QSystemTrayIcon *trayIcon; + QPixmap pixmap; + int timerId; +}; + +#if defined(Q_WS_X11) +QT_BEGIN_INCLUDE_NAMESPACE +#include <QtCore/qcoreapplication.h> +#include <X11/Xlib.h> +#include <X11/Xatom.h> +#include <X11/Xutil.h> +QT_END_INCLUDE_NAMESPACE + +class QSystemTrayIconSys : public QWidget +{ + friend class QSystemTrayIconPrivate; + +public: + QSystemTrayIconSys(QSystemTrayIcon *q); + ~QSystemTrayIconSys(); + enum { + SYSTEM_TRAY_REQUEST_DOCK = 0, + SYSTEM_TRAY_BEGIN_MESSAGE = 1, + SYSTEM_TRAY_CANCEL_MESSAGE =2 + }; + + void addToTray(); + void updateIcon(); + XVisualInfo* getSysTrayVisualInfo(); + + // QObject::event is public but QWidget's ::event() re-implementation + // is protected ;( + inline bool deliverToolTipEvent(QEvent *e) + { return QWidget::event(e); } + + static Window sysTrayWindow; + static QList<QSystemTrayIconSys *> trayIcons; + static QCoreApplication::EventFilter oldEventFilter; + static bool sysTrayTracker(void *message, long *result); + static Window locateSystemTray(); + static Atom sysTraySelection; + static XVisualInfo sysTrayVisual; + +protected: + void paintEvent(QPaintEvent *pe); + void resizeEvent(QResizeEvent *re); + bool x11Event(XEvent *event); + void mousePressEvent(QMouseEvent *event); + void mouseDoubleClickEvent(QMouseEvent *event); +#ifndef QT_NO_WHEELEVENT + void wheelEvent(QWheelEvent *event); +#endif + bool event(QEvent *e); + +private: + QPixmap background; + QSystemTrayIcon *q; + Colormap colormap; +}; +#endif // Q_WS_X11 + +QT_END_NAMESPACE + +#endif // QT_NO_SYSTEMTRAYICON + +#endif // QSYSTEMTRAYICON_P_H + diff --git a/src/widgets/util/qsystemtrayicon_qpa.cpp b/src/widgets/util/qsystemtrayicon_qpa.cpp new file mode 100644 index 0000000000..48f8fe8922 --- /dev/null +++ b/src/widgets/util/qsystemtrayicon_qpa.cpp @@ -0,0 +1,96 @@ +/**************************************************************************** +** +** Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies). +** All rights reserved. +** Contact: Nokia Corporation (qt-info@nokia.com) +** +** This file is part of the QtGui module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** No Commercial Usage +** This file contains pre-release code and may not be distributed. +** You may use this file in accordance with the terms and conditions +** contained in the Technology Preview License Agreement accompanying +** this package. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain additional +** rights. These rights are described in the Nokia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** If you have questions regarding the use of this file, please contact +** Nokia at qt-info@nokia.com. +** +** +** +** +** +** +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qsystemtrayicon_p.h" + +#ifndef QT_NO_SYSTEMTRAYICON + +QT_BEGIN_NAMESPACE + +void QSystemTrayIconPrivate::install_sys() +{ +} + +void QSystemTrayIconPrivate::remove_sys() +{ +} + +QRect QSystemTrayIconPrivate::geometry_sys() const +{ + return QRect(); +} + +void QSystemTrayIconPrivate::updateIcon_sys() +{ +} + +void QSystemTrayIconPrivate::updateMenu_sys() +{ +} + +void QSystemTrayIconPrivate::updateToolTip_sys() +{ +} + +bool QSystemTrayIconPrivate::isSystemTrayAvailable_sys() +{ + return false; +} + +bool QSystemTrayIconPrivate::supportsMessages_sys() +{ + return false; +} + +void QSystemTrayIconPrivate::showMessage_sys(const QString &message, + const QString &title, + QSystemTrayIcon::MessageIcon icon, + int msecs) +{ + Q_UNUSED(message); + Q_UNUSED(title); + Q_UNUSED(icon); + Q_UNUSED(msecs); +} + +QT_END_NAMESPACE + +#endif // QT_NO_SYSTEMTRAYICON diff --git a/src/widgets/util/qsystemtrayicon_win.cpp b/src/widgets/util/qsystemtrayicon_win.cpp new file mode 100644 index 0000000000..5a0e179e6d --- /dev/null +++ b/src/widgets/util/qsystemtrayicon_win.cpp @@ -0,0 +1,524 @@ +/**************************************************************************** +** +** Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies). +** All rights reserved. +** Contact: Nokia Corporation (qt-info@nokia.com) +** +** This file is part of the QtGui module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** No Commercial Usage +** This file contains pre-release code and may not be distributed. +** You may use this file in accordance with the terms and conditions +** contained in the Technology Preview License Agreement accompanying +** this package. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain additional +** rights. These rights are described in the Nokia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** If you have questions regarding the use of this file, please contact +** Nokia at qt-info@nokia.com. +** +** +** +** +** +** +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qsystemtrayicon_p.h" +#ifndef QT_NO_SYSTEMTRAYICON + +#ifndef _WIN32_WINNT +#define _WIN32_WINNT 0x0600 +#endif + +#ifndef _WIN32_IE +#define _WIN32_IE 0x600 +#endif + +#include <qt_windows.h> +#include <windowsx.h> +#include <commctrl.h> + +#include <private/qsystemlibrary_p.h> +#include <QApplication> +#include <QSettings> + +QT_BEGIN_NAMESPACE + +static const UINT q_uNOTIFYICONID = 0; + +static uint MYWM_TASKBARCREATED = 0; +#define MYWM_NOTIFYICON (WM_APP+101) + +struct Q_NOTIFYICONIDENTIFIER { + DWORD cbSize; + HWND hWnd; + UINT uID; + GUID guidItem; +}; + +#ifndef NOTIFYICON_VERSION_4 +#define NOTIFYICON_VERSION_4 4 +#endif + +#ifndef NIN_SELECT +#define NIN_SELECT (WM_USER + 0) +#endif + +#ifndef NIN_KEYSELECT +#define NIN_KEYSELECT (WM_USER + 1) +#endif + +#ifndef NIN_BALLOONTIMEOUT +#define NIN_BALLOONTIMEOUT (WM_USER + 4) +#endif + +#ifndef NIN_BALLOONUSERCLICK +#define NIN_BALLOONUSERCLICK (WM_USER + 5) +#endif + +#ifndef NIF_SHOWTIP +#define NIF_SHOWTIP 0x00000080 +#endif + +#define Q_MSGFLT_ALLOW 1 + +typedef HRESULT (WINAPI *PtrShell_NotifyIconGetRect)(const Q_NOTIFYICONIDENTIFIER* identifier, RECT* iconLocation); +typedef BOOL (WINAPI *PtrChangeWindowMessageFilter)(UINT message, DWORD dwFlag); +typedef BOOL (WINAPI *PtrChangeWindowMessageFilterEx)(HWND hWnd, UINT message, DWORD action, void* pChangeFilterStruct); + +class QSystemTrayIconSys : QWidget +{ +public: + QSystemTrayIconSys(QSystemTrayIcon *object); + ~QSystemTrayIconSys(); + bool winEvent( MSG *m, long *result ); + bool trayMessage(DWORD msg); + void setIconContents(NOTIFYICONDATA &data); + bool showMessage(const QString &title, const QString &message, QSystemTrayIcon::MessageIcon type, uint uSecs); + QRect findIconGeometry(const int a_iButtonID); + void createIcon(); + HICON hIcon; + QPoint globalPos; + QSystemTrayIcon *q; +private: + uint notifyIconSize; + int maxTipLength; + int version; + bool ignoreNextMouseRelease; +}; + +static bool allowsMessages() +{ +#ifndef QT_NO_SETTINGS + QSettings settings(QLatin1String("HKEY_CURRENT_USER\\Software\\Microsoft" + "\\Windows\\CurrentVersion\\Explorer\\Advanced"), QSettings::NativeFormat); + return settings.value(QLatin1String("EnableBalloonTips"), true).toBool(); +#else + return false; +#endif +} + +QSystemTrayIconSys::QSystemTrayIconSys(QSystemTrayIcon *object) + : hIcon(0), q(object), ignoreNextMouseRelease(false) + +{ + if (QSysInfo::windowsVersion() >= QSysInfo::WV_VISTA) { + notifyIconSize = sizeof(NOTIFYICONDATA); + version = NOTIFYICON_VERSION_4; + } else { + notifyIconSize = NOTIFYICONDATA_V2_SIZE; + version = NOTIFYICON_VERSION; + } + + maxTipLength = 128; + + // For restoring the tray icon after explorer crashes + if (!MYWM_TASKBARCREATED) { + MYWM_TASKBARCREATED = RegisterWindowMessage(L"TaskbarCreated"); + } + + // Allow the WM_TASKBARCREATED message through the UIPI filter on Windows Vista and higher + static PtrChangeWindowMessageFilterEx pChangeWindowMessageFilterEx = + (PtrChangeWindowMessageFilterEx)QSystemLibrary::resolve(QLatin1String("user32"), "ChangeWindowMessageFilterEx"); + + if (pChangeWindowMessageFilterEx) { + // Call the safer ChangeWindowMessageFilterEx API if available + pChangeWindowMessageFilterEx(winId(), MYWM_TASKBARCREATED, Q_MSGFLT_ALLOW, 0); + } else { + static PtrChangeWindowMessageFilter pChangeWindowMessageFilter = + (PtrChangeWindowMessageFilter)QSystemLibrary::resolve(QLatin1String("user32"), "ChangeWindowMessageFilter"); + + if (pChangeWindowMessageFilter) { + // Call the deprecated ChangeWindowMessageFilter API otherwise + pChangeWindowMessageFilter(MYWM_TASKBARCREATED, Q_MSGFLT_ALLOW); + } + } +} + +QSystemTrayIconSys::~QSystemTrayIconSys() +{ + if (hIcon) + DestroyIcon(hIcon); +} + +void QSystemTrayIconSys::setIconContents(NOTIFYICONDATA &tnd) +{ + tnd.uFlags |= NIF_MESSAGE | NIF_ICON | NIF_TIP; + tnd.uCallbackMessage = MYWM_NOTIFYICON; + tnd.hIcon = hIcon; + QString tip = q->toolTip(); + + if (!tip.isNull()) { + tip = tip.left(maxTipLength - 1) + QChar(); + memcpy(tnd.szTip, tip.utf16(), qMin(tip.length() + 1, maxTipLength) * sizeof(wchar_t)); + } +} + +static int iconFlag( QSystemTrayIcon::MessageIcon icon ) +{ + switch (icon) { + case QSystemTrayIcon::Information: + return NIIF_INFO; + case QSystemTrayIcon::Warning: + return NIIF_WARNING; + case QSystemTrayIcon::Critical: + return NIIF_ERROR; + case QSystemTrayIcon::NoIcon: + return NIIF_NONE; + default: + Q_ASSERT_X(false, "QSystemTrayIconSys::showMessage", "Invalid QSystemTrayIcon::MessageIcon value"); + return NIIF_NONE; + } +} + +bool QSystemTrayIconSys::showMessage(const QString &title, const QString &message, QSystemTrayIcon::MessageIcon type, uint uSecs) +{ + NOTIFYICONDATA tnd; + memset(&tnd, 0, notifyIconSize); + + memcpy(tnd.szInfo, message.utf16(), qMin(message.length() + 1, 256) * sizeof(wchar_t)); + memcpy(tnd.szInfoTitle, title.utf16(), qMin(title.length() + 1, 64) * sizeof(wchar_t)); + + tnd.uID = q_uNOTIFYICONID; + tnd.dwInfoFlags = iconFlag(type); + tnd.cbSize = notifyIconSize; + tnd.hWnd = winId(); + tnd.uTimeout = uSecs; + tnd.uFlags = NIF_INFO | NIF_SHOWTIP; + + Q_ASSERT(testAttribute(Qt::WA_WState_Created)); + + return Shell_NotifyIcon(NIM_MODIFY, &tnd); +} + +bool QSystemTrayIconSys::trayMessage(DWORD msg) +{ + NOTIFYICONDATA tnd; + memset(&tnd, 0, notifyIconSize); + + tnd.uID = q_uNOTIFYICONID; + tnd.cbSize = notifyIconSize; + tnd.hWnd = winId(); + tnd.uFlags = NIF_SHOWTIP; + tnd.uVersion = version; + + Q_ASSERT(testAttribute(Qt::WA_WState_Created)); + + if (msg == NIM_ADD || msg == NIM_MODIFY) { + setIconContents(tnd); + } + + bool success = Shell_NotifyIcon(msg, &tnd); + + if (msg == NIM_ADD) + return success && Shell_NotifyIcon(NIM_SETVERSION, &tnd); + else + return success; +} + +void QSystemTrayIconSys::createIcon() +{ + hIcon = 0; + QIcon icon = q->icon(); + if (icon.isNull()) + return; + + const int iconSizeX = GetSystemMetrics(SM_CXSMICON); + const int iconSizeY = GetSystemMetrics(SM_CYSMICON); + QSize size = icon.actualSize(QSize(iconSizeX, iconSizeY)); + QPixmap pm = icon.pixmap(size); + if (pm.isNull()) + return; + + hIcon = pm.toWinHICON(); +} + +bool QSystemTrayIconSys::winEvent( MSG *m, long *result ) +{ + switch(m->message) { + case MYWM_NOTIFYICON: + { + int message = 0; + QPoint gpos; + + if (version == NOTIFYICON_VERSION_4) { + Q_ASSERT(q_uNOTIFYICONID == HIWORD(m->lParam)); + message = LOWORD(m->lParam); + gpos = QPoint(GET_X_LPARAM(m->wParam), GET_Y_LPARAM(m->wParam)); + } else { + Q_ASSERT(q_uNOTIFYICONID == m->wParam); + message = m->lParam; + gpos = QCursor::pos(); + } + + switch (message) { + case NIN_SELECT: + case NIN_KEYSELECT: + if (ignoreNextMouseRelease) + ignoreNextMouseRelease = false; + else + emit q->activated(QSystemTrayIcon::Trigger); + break; + + case WM_LBUTTONDBLCLK: + ignoreNextMouseRelease = true; // Since DBLCLICK Generates a second mouse + // release we must ignore it + emit q->activated(QSystemTrayIcon::DoubleClick); + break; + + case WM_CONTEXTMENU: + if (q->contextMenu()) { + q->contextMenu()->popup(gpos); + q->contextMenu()->activateWindow(); + } + emit q->activated(QSystemTrayIcon::Context); + break; + + case NIN_BALLOONUSERCLICK: + emit q->messageClicked(); + break; + + case WM_MBUTTONUP: + emit q->activated(QSystemTrayIcon::MiddleClick); + break; + + default: + break; + } + break; + } + default: + if (m->message == MYWM_TASKBARCREATED) + trayMessage(NIM_ADD); + else + return QWidget::winEvent(m, result); + break; + } + return 0; +} + +void QSystemTrayIconPrivate::install_sys() +{ + Q_Q(QSystemTrayIcon); + if (!sys) { + sys = new QSystemTrayIconSys(q); + sys->createIcon(); + sys->trayMessage(NIM_ADD); + } +} + +/* +* This function tries to determine the icon geometry from the tray +* +* If it fails an invalid rect is returned. +*/ +QRect QSystemTrayIconSys::findIconGeometry(const int iconId) +{ + static PtrShell_NotifyIconGetRect Shell_NotifyIconGetRect = + (PtrShell_NotifyIconGetRect)QSystemLibrary::resolve(QLatin1String("shell32"), "Shell_NotifyIconGetRect"); + + if (Shell_NotifyIconGetRect) { + Q_NOTIFYICONIDENTIFIER nid; + memset(&nid, 0, sizeof(nid)); + nid.cbSize = sizeof(nid); + nid.hWnd = winId(); + nid.uID = iconId; + + RECT rect; + HRESULT hr = Shell_NotifyIconGetRect(&nid, &rect); + if (SUCCEEDED(hr)) { + return QRect(rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top); + } + } + + QRect ret; + + TBBUTTON buttonData; + DWORD processID = 0; + HWND trayHandle = FindWindow(L"Shell_TrayWnd", NULL); + + //find the toolbar used in the notification area + if (trayHandle) { + trayHandle = FindWindowEx(trayHandle, NULL, L"TrayNotifyWnd", NULL); + if (trayHandle) { + HWND hwnd = FindWindowEx(trayHandle, NULL, L"SysPager", NULL); + if (hwnd) { + hwnd = FindWindowEx(hwnd, NULL, L"ToolbarWindow32", NULL); + if (hwnd) + trayHandle = hwnd; + } + } + } + + if (!trayHandle) + return ret; + + GetWindowThreadProcessId(trayHandle, &processID); + if (processID <= 0) + return ret; + + HANDLE trayProcess = OpenProcess(PROCESS_VM_OPERATION | PROCESS_VM_READ, 0, processID); + if (!trayProcess) + return ret; + + int buttonCount = SendMessage(trayHandle, TB_BUTTONCOUNT, 0, 0); + LPVOID data = VirtualAllocEx(trayProcess, NULL, sizeof(TBBUTTON), MEM_COMMIT, PAGE_READWRITE); + + if ( buttonCount < 1 || !data ) { + CloseHandle(trayProcess); + return ret; + } + + //search for our icon among all toolbar buttons + for (int toolbarButton = 0; toolbarButton < buttonCount; ++toolbarButton ) { + SIZE_T numBytes = 0; + DWORD appData[2] = { 0, 0 }; + SendMessage(trayHandle, TB_GETBUTTON, toolbarButton , (LPARAM)data); + + if (!ReadProcessMemory(trayProcess, data, &buttonData, sizeof(TBBUTTON), &numBytes)) + continue; + + if (!ReadProcessMemory(trayProcess, (LPVOID) buttonData.dwData, appData, sizeof(appData), &numBytes)) + continue; + + int currentIconId = appData[1]; + HWND currentIconHandle = (HWND) appData[0]; + bool isHidden = buttonData.fsState & TBSTATE_HIDDEN; + + if (currentIconHandle == winId() && + currentIconId == iconId && !isHidden) { + SendMessage(trayHandle, TB_GETITEMRECT, toolbarButton , (LPARAM)data); + RECT iconRect = {0, 0}; + if(ReadProcessMemory(trayProcess, data, &iconRect, sizeof(RECT), &numBytes)) { + MapWindowPoints(trayHandle, NULL, (LPPOINT)&iconRect, 2); + QRect geometry(iconRect.left + 1, iconRect.top + 1, + iconRect.right - iconRect.left - 2, + iconRect.bottom - iconRect.top - 2); + if (geometry.isValid()) + ret = geometry; + break; + } + } + } + VirtualFreeEx(trayProcess, data, 0, MEM_RELEASE); + CloseHandle(trayProcess); + return ret; +} + +void QSystemTrayIconPrivate::showMessage_sys(const QString &title, const QString &message, QSystemTrayIcon::MessageIcon type, int timeOut) +{ + if (!sys || !allowsMessages()) + return; + + uint uSecs = 0; + if ( timeOut < 0) + uSecs = 10000; //10 sec default + else uSecs = (int)timeOut; + + //message is limited to 255 chars + NULL + QString messageString; + if (message.isEmpty() && !title.isEmpty()) + messageString = QLatin1Char(' '); //ensures that the message shows when only title is set + else + messageString = message.left(255) + QChar(); + + //title is limited to 63 chars + NULL + QString titleString = title.left(63) + QChar(); + + sys->showMessage(titleString, messageString, type, uSecs); +} + +QRect QSystemTrayIconPrivate::geometry_sys() const +{ + if (!sys) + return QRect(); + + return sys->findIconGeometry(q_uNOTIFYICONID); +} + +void QSystemTrayIconPrivate::remove_sys() +{ + if (!sys) + return; + + sys->trayMessage(NIM_DELETE); + delete sys; + sys = 0; +} + +void QSystemTrayIconPrivate::updateIcon_sys() +{ + if (!sys) + return; + + HICON hIconToDestroy = sys->hIcon; + + sys->createIcon(); + sys->trayMessage(NIM_MODIFY); + + if (hIconToDestroy) + DestroyIcon(hIconToDestroy); +} + +void QSystemTrayIconPrivate::updateMenu_sys() +{ + +} + +void QSystemTrayIconPrivate::updateToolTip_sys() +{ + if (!sys) + return; + + sys->trayMessage(NIM_MODIFY); +} + +bool QSystemTrayIconPrivate::isSystemTrayAvailable_sys() +{ + return true; +} + +bool QSystemTrayIconPrivate::supportsMessages_sys() +{ + return allowsMessages(); +} + +QT_END_NAMESPACE + +#endif diff --git a/src/widgets/util/qsystemtrayicon_wince.cpp b/src/widgets/util/qsystemtrayicon_wince.cpp new file mode 100644 index 0000000000..85a34bbe25 --- /dev/null +++ b/src/widgets/util/qsystemtrayicon_wince.cpp @@ -0,0 +1,293 @@ +/**************************************************************************** +** +** Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies). +** All rights reserved. +** Contact: Nokia Corporation (qt-info@nokia.com) +** +** This file is part of the QtGui module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** No Commercial Usage +** This file contains pre-release code and may not be distributed. +** You may use this file in accordance with the terms and conditions +** contained in the Technology Preview License Agreement accompanying +** this package. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain additional +** rights. These rights are described in the Nokia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** If you have questions regarding the use of this file, please contact +** Nokia at qt-info@nokia.com. +** +** +** +** +** +** +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qsystemtrayicon_p.h" +#ifndef QT_NO_SYSTEMTRAYICON +#define _WIN32_IE 0x0600 //required for NOTIFYICONDATA_V2_SIZE + +#include <qt_windows.h> +#include <shlwapi.h> +#include <QApplication> + +QT_BEGIN_NAMESPACE + +static const UINT q_uNOTIFYICONID = 13; // IDs from 0 to 12 are reserved on WinCE. +#define MYWM_NOTIFYICON (WM_APP+101) + +struct Q_NOTIFYICONIDENTIFIER { + DWORD cbSize; + HWND hWnd; + UINT uID; + GUID guidItem; +}; + +class QSystemTrayIconSys : QWidget +{ +public: + QSystemTrayIconSys(QSystemTrayIcon *object); + ~QSystemTrayIconSys(); + bool winEvent( MSG *m, long *result ); + bool trayMessage(DWORD msg); + void setIconContents(NOTIFYICONDATA &data); + void createIcon(); + QRect findTrayGeometry(); + HICON hIcon; + QPoint globalPos; + QSystemTrayIcon *q; +private: + uint notifyIconSize; + int maxTipLength; + bool ignoreNextMouseRelease; +}; + +QSystemTrayIconSys::QSystemTrayIconSys(QSystemTrayIcon *object) + : hIcon(0), q(object), ignoreNextMouseRelease(false) + +{ + notifyIconSize = FIELD_OFFSET(NOTIFYICONDATA, szTip[64]); // NOTIFYICONDATAW_V1_SIZE; + maxTipLength = 64; +} + +QSystemTrayIconSys::~QSystemTrayIconSys() +{ + if (hIcon) + DestroyIcon(hIcon); +} + +QRect QSystemTrayIconSys::findTrayGeometry() +{ + // Use lower right corner as fallback + QPoint brCorner = qApp->desktop()->screenGeometry().bottomRight(); + QRect ret(brCorner.x() - 10, brCorner.y() - 10, 10, 10); + return ret; +} + +void QSystemTrayIconSys::setIconContents(NOTIFYICONDATA &tnd) +{ + tnd.uFlags = NIF_MESSAGE | NIF_ICON | NIF_TIP; + tnd.uCallbackMessage = MYWM_NOTIFYICON; + tnd.hIcon = hIcon; + QString tip = q->toolTip(); + + if (!tip.isNull()) { + tip = tip.left(maxTipLength - 1) + QChar(); + memcpy(tnd.szTip, tip.utf16(), qMin(tip.length() + 1, maxTipLength) * sizeof(wchar_t)); + } +} + +bool QSystemTrayIconSys::trayMessage(DWORD msg) +{ + NOTIFYICONDATA tnd; + memset(&tnd, 0, notifyIconSize); + tnd.uID = q_uNOTIFYICONID; + tnd.cbSize = notifyIconSize; + tnd.hWnd = winId(); + + Q_ASSERT(testAttribute(Qt::WA_WState_Created)); + + if (msg != NIM_DELETE) { + setIconContents(tnd); + } + + return Shell_NotifyIcon(msg, &tnd); +} + +void QSystemTrayIconSys::createIcon() +{ + hIcon = 0; + QIcon icon = q->icon(); + if (icon.isNull()) + return; + + //const QSize preferredSize(GetSystemMetrics(SM_CXSMICON) * 2, GetSystemMetrics(SM_CYSMICON) * 2); + const QSize preferredSize(GetSystemMetrics(SM_CXICON), GetSystemMetrics(SM_CYICON)); + QPixmap pm = icon.pixmap(preferredSize); + if (pm.isNull()) + return; + + hIcon = pm.toWinHICON(); +} + +bool QSystemTrayIconSys::winEvent( MSG *m, long *result ) +{ + switch(m->message) { + case WM_CREATE: + SetWindowLong(winId(), GWL_USERDATA, (LONG)((CREATESTRUCTW*)m->lParam)->lpCreateParams); + break; + + case MYWM_NOTIFYICON: + { + QPoint gpos = QCursor::pos(); + + switch (m->lParam) { + case WM_LBUTTONUP: + if (ignoreNextMouseRelease) + ignoreNextMouseRelease = false; + else + emit q->activated(QSystemTrayIcon::Trigger); + break; + + case WM_LBUTTONDBLCLK: + ignoreNextMouseRelease = true; // Since DBLCLICK Generates a second mouse + // release we must ignore it + emit q->activated(QSystemTrayIcon::DoubleClick); + break; + + case WM_RBUTTONUP: + if (q->contextMenu()) { + q->contextMenu()->popup(gpos); + + // We must ensure that the popup menu doesn't show up behind the task bar. + QRect desktopRect = qApp->desktop()->availableGeometry(); + int maxY = desktopRect.y() + desktopRect.height() - q->contextMenu()->height(); + if (gpos.y() > maxY) { + gpos.ry() = maxY; + q->contextMenu()->move(gpos); + } + } + emit q->activated(QSystemTrayIcon::Context); + break; + + case WM_MBUTTONUP: + emit q->activated(QSystemTrayIcon::MiddleClick); + break; + + default: + break; + } + break; + } + default: + return QWidget::winEvent(m, result); + } + return 0; +} + +void QSystemTrayIconPrivate::install_sys() +{ + Q_Q(QSystemTrayIcon); + if (!sys) { + sys = new QSystemTrayIconSys(q); + sys->createIcon(); + sys->trayMessage(NIM_ADD); + } +} + +void QSystemTrayIconPrivate::showMessage_sys(const QString &title, const QString &message, QSystemTrayIcon::MessageIcon type, int timeOut) +{ + if (!sys) + return; + + uint uSecs = 0; + if ( timeOut < 0) + uSecs = 10000; //10 sec default + else uSecs = (int)timeOut; + + //message is limited to 255 chars + NULL + QString messageString; + if (message.isEmpty() && !title.isEmpty()) + messageString = QLatin1Char(' '); //ensures that the message shows when only title is set + else + messageString = message.left(255) + QChar(); + + //title is limited to 63 chars + NULL + QString titleString = title.left(63) + QChar(); + + //show QBalloonTip + QRect trayRect = sys->findTrayGeometry(); + QBalloonTip::showBalloon(type, title, message, sys->q, QPoint(trayRect.left(), + trayRect.center().y()), uSecs, false); +} + +QRect QSystemTrayIconPrivate::geometry_sys() const +{ + return QRect(); +} + +void QSystemTrayIconPrivate::remove_sys() +{ + if (!sys) + return; + + sys->trayMessage(NIM_DELETE); + delete sys; + sys = 0; +} + +void QSystemTrayIconPrivate::updateIcon_sys() +{ + if (!sys) + return; + + HICON hIconToDestroy = sys->hIcon; + + sys->createIcon(); + sys->trayMessage(NIM_MODIFY); + + if (hIconToDestroy) + DestroyIcon(hIconToDestroy); +} + +void QSystemTrayIconPrivate::updateMenu_sys() +{ + +} + +void QSystemTrayIconPrivate::updateToolTip_sys() +{ + // Calling sys->trayMessage(NIM_MODIFY) on an existing icon is broken on Windows CE. + // So we need to call updateIcon_sys() which creates a new icon handle. + updateIcon_sys(); +} + +bool QSystemTrayIconPrivate::isSystemTrayAvailable_sys() +{ + return true; +} + +bool QSystemTrayIconPrivate::supportsMessages_sys() +{ + return true; +} + +QT_END_NAMESPACE + +#endif diff --git a/src/widgets/util/qsystemtrayicon_x11.cpp b/src/widgets/util/qsystemtrayicon_x11.cpp new file mode 100644 index 0000000000..694746c834 --- /dev/null +++ b/src/widgets/util/qsystemtrayicon_x11.cpp @@ -0,0 +1,401 @@ +/**************************************************************************** +** +** Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies). +** All rights reserved. +** Contact: Nokia Corporation (qt-info@nokia.com) +** +** This file is part of the QtGui module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** No Commercial Usage +** This file contains pre-release code and may not be distributed. +** You may use this file in accordance with the terms and conditions +** contained in the Technology Preview License Agreement accompanying +** this package. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain additional +** rights. These rights are described in the Nokia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** If you have questions regarding the use of this file, please contact +** Nokia at qt-info@nokia.com. +** +** +** +** +** +** +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "private/qt_x11_p.h" +#include "qlabel.h" +#include "qx11info_x11.h" +#include "qpainter.h" +#include "qpixmap.h" +#include "qbitmap.h" +#include "qevent.h" +#include "qapplication.h" +#include "qlist.h" +#include "qmenu.h" +#include "qtimer.h" +#include "qsystemtrayicon_p.h" +#include "qpaintengine.h" + +#ifndef QT_NO_SYSTEMTRAYICON +QT_BEGIN_NAMESPACE + +Window QSystemTrayIconSys::sysTrayWindow = XNone; +QList<QSystemTrayIconSys *> QSystemTrayIconSys::trayIcons; +QCoreApplication::EventFilter QSystemTrayIconSys::oldEventFilter = 0; +Atom QSystemTrayIconSys::sysTraySelection = XNone; +XVisualInfo QSystemTrayIconSys::sysTrayVisual = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + +// Locate the system tray +Window QSystemTrayIconSys::locateSystemTray() +{ + Display *display = QX11Info::display(); + if (sysTraySelection == XNone) { + int screen = QX11Info::appScreen(); + QString net_sys_tray = QString::fromLatin1("_NET_SYSTEM_TRAY_S%1").arg(screen); + sysTraySelection = XInternAtom(display, net_sys_tray.toLatin1(), False); + } + + return XGetSelectionOwner(QX11Info::display(), sysTraySelection); +} + +XVisualInfo* QSystemTrayIconSys::getSysTrayVisualInfo() +{ + Display *display = QX11Info::display(); + + if (!sysTrayVisual.visual) { + Window win = locateSystemTray(); + if (win != XNone) { + Atom actual_type; + int actual_format; + ulong nitems, bytes_remaining; + uchar *data = 0; + int result = XGetWindowProperty(display, win, ATOM(_NET_SYSTEM_TRAY_VISUAL), 0, 1, + False, XA_VISUALID, &actual_type, + &actual_format, &nitems, &bytes_remaining, &data); + VisualID vid = 0; + if (result == Success && data && actual_type == XA_VISUALID && actual_format == 32 && + nitems == 1 && bytes_remaining == 0) + vid = *(VisualID*)data; + if (data) + XFree(data); + if (vid == 0) + return 0; + + uint mask = VisualIDMask; + XVisualInfo *vi, rvi; + int count; + rvi.visualid = vid; + vi = XGetVisualInfo(display, mask, &rvi, &count); + if (vi) { + sysTrayVisual = vi[0]; + XFree((char*)vi); + } + if (sysTrayVisual.depth != 32) + memset(&sysTrayVisual, 0, sizeof(sysTrayVisual)); + } + } + + return sysTrayVisual.visual ? &sysTrayVisual : 0; +} + +bool QSystemTrayIconSys::sysTrayTracker(void *message, long *result) +{ + bool retval = false; + if (QSystemTrayIconSys::oldEventFilter) + retval = QSystemTrayIconSys::oldEventFilter(message, result); + + if (trayIcons.isEmpty()) + return retval; + + Display *display = QX11Info::display(); + XEvent *ev = (XEvent *)message; + if (ev->type == DestroyNotify && ev->xany.window == sysTrayWindow) { + sysTrayWindow = locateSystemTray(); + memset(&sysTrayVisual, 0, sizeof(sysTrayVisual)); + for (int i = 0; i < trayIcons.count(); i++) { + if (sysTrayWindow == XNone) { + QBalloonTip::hideBalloon(); + trayIcons[i]->hide(); // still no luck + trayIcons[i]->destroy(); + trayIcons[i]->create(); + } else + trayIcons[i]->addToTray(); // add it to the new tray + } + retval = true; + } else if (ev->type == ClientMessage && sysTrayWindow == XNone) { + static Atom manager_atom = XInternAtom(display, "MANAGER", False); + XClientMessageEvent *cm = (XClientMessageEvent *)message; + if ((cm->message_type == manager_atom) && ((Atom)cm->data.l[1] == sysTraySelection)) { + sysTrayWindow = cm->data.l[2]; + memset(&sysTrayVisual, 0, sizeof(sysTrayVisual)); + XSelectInput(display, sysTrayWindow, StructureNotifyMask); + for (int i = 0; i < trayIcons.count(); i++) { + trayIcons[i]->addToTray(); + } + retval = true; + } + } else if (ev->type == PropertyNotify && ev->xproperty.atom == ATOM(_NET_SYSTEM_TRAY_VISUAL) && + ev->xproperty.window == sysTrayWindow) { + memset(&sysTrayVisual, 0, sizeof(sysTrayVisual)); + for (int i = 0; i < trayIcons.count(); i++) { + trayIcons[i]->addToTray(); + } + } + + return retval; +} + +QSystemTrayIconSys::QSystemTrayIconSys(QSystemTrayIcon *q) + : QWidget(0, Qt::FramelessWindowHint | Qt::X11BypassWindowManagerHint), + q(q), colormap(0) +{ + setAttribute(Qt::WA_AlwaysShowToolTips); + setAttribute(Qt::WA_QuitOnClose, false); + setAttribute(Qt::WA_NoSystemBackground, true); + setAttribute(Qt::WA_PaintOnScreen); + + static bool eventFilterAdded = false; + Display *display = QX11Info::display(); + if (!eventFilterAdded) { + oldEventFilter = qApp->setEventFilter(sysTrayTracker); + eventFilterAdded = true; + Window root = QX11Info::appRootWindow(); + XWindowAttributes attr; + XGetWindowAttributes(display, root, &attr); + if ((attr.your_event_mask & StructureNotifyMask) != StructureNotifyMask) { + (void) QApplication::desktop(); // lame trick to ensure our event mask is not overridden + XSelectInput(display, root, attr.your_event_mask | StructureNotifyMask); // for MANAGER selection + } + } + if (trayIcons.isEmpty()) { + sysTrayWindow = locateSystemTray(); + if (sysTrayWindow != XNone) + XSelectInput(display, sysTrayWindow, StructureNotifyMask); // track tray events + } + trayIcons.append(this); + setMouseTracking(true); +#ifndef QT_NO_TOOLTIP + setToolTip(q->toolTip()); +#endif + if (sysTrayWindow != XNone) + addToTray(); +} + +QSystemTrayIconSys::~QSystemTrayIconSys() +{ + trayIcons.removeAt(trayIcons.indexOf(this)); + Display *display = QX11Info::display(); + if (trayIcons.isEmpty()) { + if (sysTrayWindow == XNone) + return; + if (display) + XSelectInput(display, sysTrayWindow, 0); // stop tracking the tray + sysTrayWindow = XNone; + } + if (colormap) + XFreeColormap(display, colormap); +} + +void QSystemTrayIconSys::addToTray() +{ + Q_ASSERT(sysTrayWindow != XNone); + Display *display = QX11Info::display(); + + XVisualInfo *vi = getSysTrayVisualInfo(); + if (vi && vi->visual) { + Window root = RootWindow(display, vi->screen); + Window p = root; + if (QWidget *pw = parentWidget()) + p = pw->effectiveWinId(); + colormap = XCreateColormap(display, root, vi->visual, AllocNone); + XSetWindowAttributes wsa; + wsa.background_pixmap = 0; + wsa.colormap = colormap; + wsa.background_pixel = 0; + wsa.border_pixel = 0; + Window wid = XCreateWindow(display, p, -1, -1, 1, 1, + 0, vi->depth, InputOutput, vi->visual, + CWBackPixmap|CWBackPixel|CWBorderPixel|CWColormap, &wsa); + create(wid); + } else { + XSetWindowBackgroundPixmap(display, winId(), ParentRelative); + } + + // GNOME, NET WM Specification + static Atom netwm_tray_atom = XInternAtom(display, "_NET_SYSTEM_TRAY_OPCODE", False); + long l[5] = { CurrentTime, SYSTEM_TRAY_REQUEST_DOCK, winId(), 0, 0 }; + XEvent ev; + memset(&ev, 0, sizeof(ev)); + ev.xclient.type = ClientMessage; + ev.xclient.window = sysTrayWindow; + ev.xclient.message_type = netwm_tray_atom; + ev.xclient.format = 32; + memcpy((char *)&ev.xclient.data, (const char *) l, sizeof(l)); + XSendEvent(display, sysTrayWindow, False, 0, &ev); + setMinimumSize(22, 22); // required at least on GNOME +} + +void QSystemTrayIconSys::updateIcon() +{ + update(); +} + +void QSystemTrayIconSys::resizeEvent(QResizeEvent *re) +{ + QWidget::resizeEvent(re); + updateIcon(); +} + +void QSystemTrayIconSys::paintEvent(QPaintEvent*) +{ + QPainter p(this); + if (!getSysTrayVisualInfo()) { + const QRegion oldSystemClip = p.paintEngine()->systemClip(); + const QRect clearedRect = oldSystemClip.boundingRect(); + XClearArea(QX11Info::display(), winId(), clearedRect.x(), clearedRect.y(), + clearedRect.width(), clearedRect.height(), False); + QPaintEngine *pe = p.paintEngine(); + pe->setSystemClip(clearedRect); + q->icon().paint(&p, rect()); + pe->setSystemClip(oldSystemClip); + } else { + p.setCompositionMode(QPainter::CompositionMode_Source); + p.fillRect(rect(), Qt::transparent); + p.setCompositionMode(QPainter::CompositionMode_SourceOver); + q->icon().paint(&p, rect()); + } +} + +void QSystemTrayIconSys::mousePressEvent(QMouseEvent *ev) +{ + QPoint globalPos = ev->globalPos(); + if (ev->button() == Qt::RightButton && q->contextMenu()) + q->contextMenu()->popup(globalPos); + + if (QBalloonTip::isBalloonVisible()) { + emit q->messageClicked(); + QBalloonTip::hideBalloon(); + } + + if (ev->button() == Qt::LeftButton) + emit q->activated(QSystemTrayIcon::Trigger); + else if (ev->button() == Qt::RightButton) + emit q->activated(QSystemTrayIcon::Context); + else if (ev->button() == Qt::MidButton) + emit q->activated(QSystemTrayIcon::MiddleClick); +} + +void QSystemTrayIconSys::mouseDoubleClickEvent(QMouseEvent *ev) +{ + if (ev->button() == Qt::LeftButton) + emit q->activated(QSystemTrayIcon::DoubleClick); +} + +#ifndef QT_NO_WHEELEVENT +void QSystemTrayIconSys::wheelEvent(QWheelEvent *e) +{ + QApplication::sendEvent(q, e); +} +#endif + +bool QSystemTrayIconSys::event(QEvent *e) +{ + if (e->type() == QEvent::ToolTip) { + return QApplication::sendEvent(q, e); + } + return QWidget::event(e); +} + +bool QSystemTrayIconSys::x11Event(XEvent *event) +{ + if (event->type == ReparentNotify) + show(); + return QWidget::x11Event(event); +} + +//////////////////////////////////////////////////////////////////////////// +void QSystemTrayIconPrivate::install_sys() +{ + Q_Q(QSystemTrayIcon); + if (!sys) + sys = new QSystemTrayIconSys(q); +} + +QRect QSystemTrayIconPrivate::geometry_sys() const +{ + if (!sys) + return QRect(); + return QRect(sys->mapToGlobal(QPoint(0, 0)), sys->size()); +} + +void QSystemTrayIconPrivate::remove_sys() +{ + if (!sys) + return; + QBalloonTip::hideBalloon(); + sys->hide(); // this should do the trick, but... + delete sys; // wm may resize system tray only for DestroyEvents + sys = 0; +} + +void QSystemTrayIconPrivate::updateIcon_sys() +{ + if (!sys) + return; + sys->updateIcon(); +} + +void QSystemTrayIconPrivate::updateMenu_sys() +{ + +} + +void QSystemTrayIconPrivate::updateToolTip_sys() +{ + if (!sys) + return; +#ifndef QT_NO_TOOLTIP + sys->setToolTip(toolTip); +#endif +} + +bool QSystemTrayIconPrivate::isSystemTrayAvailable_sys() +{ + return QSystemTrayIconSys::locateSystemTray() != XNone; +} + +bool QSystemTrayIconPrivate::supportsMessages_sys() +{ + return true; +} + +void QSystemTrayIconPrivate::showMessage_sys(const QString &message, const QString &title, + QSystemTrayIcon::MessageIcon icon, int msecs) +{ + if (!sys) + return; + QPoint g = sys->mapToGlobal(QPoint(0, 0)); + QBalloonTip::showBalloon(icon, message, title, sys->q, + QPoint(g.x() + sys->width()/2, g.y() + sys->height()/2), + msecs); +} + +QT_END_NAMESPACE +#endif //QT_NO_SYSTEMTRAYICON diff --git a/src/widgets/util/qundogroup.cpp b/src/widgets/util/qundogroup.cpp new file mode 100644 index 0000000000..42cda7462a --- /dev/null +++ b/src/widgets/util/qundogroup.cpp @@ -0,0 +1,499 @@ +/**************************************************************************** +** +** Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies). +** All rights reserved. +** Contact: Nokia Corporation (qt-info@nokia.com) +** +** This file is part of the QtGui module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** No Commercial Usage +** This file contains pre-release code and may not be distributed. +** You may use this file in accordance with the terms and conditions +** contained in the Technology Preview License Agreement accompanying +** this package. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain additional +** rights. These rights are described in the Nokia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** If you have questions regarding the use of this file, please contact +** Nokia at qt-info@nokia.com. +** +** +** +** +** +** +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qundogroup.h" +#include "qundostack.h" +#include "qundostack_p.h" + +#ifndef QT_NO_UNDOGROUP + +QT_BEGIN_NAMESPACE + +class QUndoGroupPrivate : public QObjectPrivate +{ + Q_DECLARE_PUBLIC(QUndoGroup) +public: + QUndoGroupPrivate() : active(0) {} + + QUndoStack *active; + QList<QUndoStack*> stack_list; +}; + +/*! + \class QUndoGroup + \brief The QUndoGroup class is a group of QUndoStack objects. + \since 4.2 + + For an overview of the Qt's undo framework, see the + \link qundo.html overview\endlink. + + An application often has multiple undo stacks, one for each opened document. At the + same time, an application usually has one undo action and one redo action, which + triggers undo or redo in the active document. + + QUndoGroup is a group of QUndoStack objects, one of which may be active. It has + an undo() and redo() slot, which calls QUndoStack::undo() and QUndoStack::redo() + for the active stack. It also has the functions createUndoAction() and createRedoAction(). + The actions returned by these functions behave in the same way as those returned by + QUndoStack::createUndoAction() and QUndoStack::createRedoAction() of the active + stack. + + Stacks are added to a group with addStack() and removed with removeStack(). A stack + is implicitly added to a group when it is created with the group as its parent + QObject. + + It is the programmer's responsibility to specify which stack is active by + calling QUndoStack::setActive(), usually when the associated document window receives focus. + The active stack may also be set with setActiveStack(), and is returned by activeStack(). + + When a stack is added to a group using addStack(), the group does not take ownership + of the stack. This means the stack has to be deleted separately from the group. When + a stack is deleted, it is automatically removed from a group. A stack may belong to + only one group. Adding it to another group will cause it to be removed from the previous + group. + + A QUndoGroup is also useful in conjunction with QUndoView. If a QUndoView is + set to watch a group using QUndoView::setGroup(), it will update itself to display + the active stack. +*/ + +/*! + Creates an empty QUndoGroup object with parent \a parent. + + \sa addStack() +*/ + +QUndoGroup::QUndoGroup(QObject *parent) + : QObject(*new QUndoGroupPrivate(), parent) +{ +} + +/*! + Destroys the QUndoGroup. +*/ +QUndoGroup::~QUndoGroup() +{ + // Ensure all QUndoStacks no longer refer to this group. + Q_D(QUndoGroup); + QList<QUndoStack *>::iterator it = d->stack_list.begin(); + QList<QUndoStack *>::iterator end = d->stack_list.end(); + while (it != end) { + (*it)->d_func()->group = 0; + ++it; + } +} + +/*! + Adds \a stack to this group. The group does not take ownership of the stack. Another + way of adding a stack to a group is by specifying the group as the stack's parent + QObject in QUndoStack::QUndoStack(). In this case, the stack is deleted when the + group is deleted, in the usual manner of QObjects. + + \sa removeStack() stacks() QUndoStack::QUndoStack() +*/ + +void QUndoGroup::addStack(QUndoStack *stack) +{ + Q_D(QUndoGroup); + + if (d->stack_list.contains(stack)) + return; + d->stack_list.append(stack); + + if (QUndoGroup *other = stack->d_func()->group) + other->removeStack(stack); + stack->d_func()->group = this; +} + +/*! + Removes \a stack from this group. If the stack was the active stack in the group, + the active stack becomes 0. + + \sa addStack() stacks() QUndoStack::~QUndoStack() +*/ + +void QUndoGroup::removeStack(QUndoStack *stack) +{ + Q_D(QUndoGroup); + + if (d->stack_list.removeAll(stack) == 0) + return; + if (stack == d->active) + setActiveStack(0); + stack->d_func()->group = 0; +} + +/*! + Returns a list of stacks in this group. + + \sa addStack() removeStack() +*/ + +QList<QUndoStack*> QUndoGroup::stacks() const +{ + Q_D(const QUndoGroup); + return d->stack_list; +} + +/*! + Sets the active stack of this group to \a stack. + + If the stack is not a member of this group, this function does nothing. + + Synonymous with calling QUndoStack::setActive() on \a stack. + + The actions returned by createUndoAction() and createRedoAction() will now behave + in the same way as those returned by \a stack's QUndoStack::createUndoAction() + and QUndoStack::createRedoAction(). + + \sa QUndoStack::setActive() activeStack() +*/ + +void QUndoGroup::setActiveStack(QUndoStack *stack) +{ + Q_D(QUndoGroup); + if (d->active == stack) + return; + + if (d->active != 0) { + disconnect(d->active, SIGNAL(canUndoChanged(bool)), + this, SIGNAL(canUndoChanged(bool))); + disconnect(d->active, SIGNAL(undoTextChanged(QString)), + this, SIGNAL(undoTextChanged(QString))); + disconnect(d->active, SIGNAL(canRedoChanged(bool)), + this, SIGNAL(canRedoChanged(bool))); + disconnect(d->active, SIGNAL(redoTextChanged(QString)), + this, SIGNAL(redoTextChanged(QString))); + disconnect(d->active, SIGNAL(indexChanged(int)), + this, SIGNAL(indexChanged(int))); + disconnect(d->active, SIGNAL(cleanChanged(bool)), + this, SIGNAL(cleanChanged(bool))); + } + + d->active = stack; + + if (d->active == 0) { + emit canUndoChanged(false); + emit undoTextChanged(QString()); + emit canRedoChanged(false); + emit redoTextChanged(QString()); + emit cleanChanged(true); + emit indexChanged(0); + } else { + connect(d->active, SIGNAL(canUndoChanged(bool)), + this, SIGNAL(canUndoChanged(bool))); + connect(d->active, SIGNAL(undoTextChanged(QString)), + this, SIGNAL(undoTextChanged(QString))); + connect(d->active, SIGNAL(canRedoChanged(bool)), + this, SIGNAL(canRedoChanged(bool))); + connect(d->active, SIGNAL(redoTextChanged(QString)), + this, SIGNAL(redoTextChanged(QString))); + connect(d->active, SIGNAL(indexChanged(int)), + this, SIGNAL(indexChanged(int))); + connect(d->active, SIGNAL(cleanChanged(bool)), + this, SIGNAL(cleanChanged(bool))); + emit canUndoChanged(d->active->canUndo()); + emit undoTextChanged(d->active->undoText()); + emit canRedoChanged(d->active->canRedo()); + emit redoTextChanged(d->active->redoText()); + emit cleanChanged(d->active->isClean()); + emit indexChanged(d->active->index()); + } + + emit activeStackChanged(d->active); +} + +/*! + Returns the active stack of this group. + + If none of the stacks are active, or if the group is empty, this function + returns 0. + + \sa setActiveStack() QUndoStack::setActive() +*/ + +QUndoStack *QUndoGroup::activeStack() const +{ + Q_D(const QUndoGroup); + return d->active; +} + +/*! + Calls QUndoStack::undo() on the active stack. + + If none of the stacks are active, or if the group is empty, this function + does nothing. + + \sa redo() canUndo() setActiveStack() +*/ + +void QUndoGroup::undo() +{ + Q_D(QUndoGroup); + if (d->active != 0) + d->active->undo(); +} + +/*! + Calls QUndoStack::redo() on the active stack. + + If none of the stacks are active, or if the group is empty, this function + does nothing. + + \sa undo() canRedo() setActiveStack() +*/ + + +void QUndoGroup::redo() +{ + Q_D(QUndoGroup); + if (d->active != 0) + d->active->redo(); +} + +/*! + Returns the value of the active stack's QUndoStack::canUndo(). + + If none of the stacks are active, or if the group is empty, this function + returns false. + + \sa canRedo() setActiveStack() +*/ + +bool QUndoGroup::canUndo() const +{ + Q_D(const QUndoGroup); + return d->active != 0 && d->active->canUndo(); +} + +/*! + Returns the value of the active stack's QUndoStack::canRedo(). + + If none of the stacks are active, or if the group is empty, this function + returns false. + + \sa canUndo() setActiveStack() +*/ + +bool QUndoGroup::canRedo() const +{ + Q_D(const QUndoGroup); + return d->active != 0 && d->active->canRedo(); +} + +/*! + Returns the value of the active stack's QUndoStack::undoText(). + + If none of the stacks are active, or if the group is empty, this function + returns an empty string. + + \sa redoText() setActiveStack() +*/ + +QString QUndoGroup::undoText() const +{ + Q_D(const QUndoGroup); + return d->active == 0 ? QString() : d->active->undoText(); +} + +/*! + Returns the value of the active stack's QUndoStack::redoText(). + + If none of the stacks are active, or if the group is empty, this function + returns an empty string. + + \sa undoText() setActiveStack() +*/ + +QString QUndoGroup::redoText() const +{ + Q_D(const QUndoGroup); + return d->active == 0 ? QString() : d->active->redoText(); +} + +/*! + Returns the value of the active stack's QUndoStack::isClean(). + + If none of the stacks are active, or if the group is empty, this function + returns true. + + \sa setActiveStack() +*/ + +bool QUndoGroup::isClean() const +{ + Q_D(const QUndoGroup); + return d->active == 0 || d->active->isClean(); +} + +#ifndef QT_NO_ACTION + +/*! + Creates an undo QAction object with parent \a parent. + + Triggering this action will cause a call to QUndoStack::undo() on the active stack. + The text of this action will always be the text of the command which will be undone + in the next call to undo(), prefixed by \a prefix. If there is no command available + for undo, if the group is empty or if none of the stacks are active, this action will + be disabled. + + If \a prefix is empty, the default prefix "Undo" is used. + + \sa createRedoAction() canUndo() QUndoCommand::text() +*/ + +QAction *QUndoGroup::createUndoAction(QObject *parent, const QString &prefix) const +{ + QString pref = prefix.isEmpty() ? tr("Undo") : prefix; + QUndoAction *result = new QUndoAction(pref, parent); + result->setEnabled(canUndo()); + result->setPrefixedText(undoText()); + connect(this, SIGNAL(canUndoChanged(bool)), + result, SLOT(setEnabled(bool))); + connect(this, SIGNAL(undoTextChanged(QString)), + result, SLOT(setPrefixedText(QString))); + connect(result, SIGNAL(triggered()), this, SLOT(undo())); + return result; +} + +/*! + Creates an redo QAction object with parent \a parent. + + Triggering this action will cause a call to QUndoStack::redo() on the active stack. + The text of this action will always be the text of the command which will be redone + in the next call to redo(), prefixed by \a prefix. If there is no command available + for redo, if the group is empty or if none of the stacks are active, this action will + be disabled. + + If \a prefix is empty, the default prefix "Undo" is used. + + \sa createUndoAction() canRedo() QUndoCommand::text() +*/ + +QAction *QUndoGroup::createRedoAction(QObject *parent, const QString &prefix) const +{ + QString pref = prefix.isEmpty() ? tr("Redo") : prefix; + QUndoAction *result = new QUndoAction(pref, parent); + result->setEnabled(canRedo()); + result->setPrefixedText(redoText()); + connect(this, SIGNAL(canRedoChanged(bool)), + result, SLOT(setEnabled(bool))); + connect(this, SIGNAL(redoTextChanged(QString)), + result, SLOT(setPrefixedText(QString))); + connect(result, SIGNAL(triggered()), this, SLOT(redo())); + return result; +} + +#endif // QT_NO_ACTION + +/*! \fn void QUndoGroup::activeStackChanged(QUndoStack *stack) + + This signal is emitted whenever the active stack of the group changes. This can happen + when setActiveStack() or QUndoStack::setActive() is called, or when the active stack + is removed form the group. \a stack is the new active stack. If no stack is active, + \a stack is 0. + + \sa setActiveStack() QUndoStack::setActive() +*/ + +/*! \fn void QUndoGroup::indexChanged(int idx) + + This signal is emitted whenever the active stack emits QUndoStack::indexChanged() + or the active stack changes. + + \a idx is the new current index, or 0 if the active stack is 0. + + \sa QUndoStack::indexChanged() setActiveStack() +*/ + +/*! \fn void QUndoGroup::cleanChanged(bool clean) + + This signal is emitted whenever the active stack emits QUndoStack::cleanChanged() + or the active stack changes. + + \a clean is the new state, or true if the active stack is 0. + + \sa QUndoStack::cleanChanged() setActiveStack() +*/ + +/*! \fn void QUndoGroup::canUndoChanged(bool canUndo) + + This signal is emitted whenever the active stack emits QUndoStack::canUndoChanged() + or the active stack changes. + + \a canUndo is the new state, or false if the active stack is 0. + + \sa QUndoStack::canUndoChanged() setActiveStack() +*/ + +/*! \fn void QUndoGroup::canRedoChanged(bool canRedo) + + This signal is emitted whenever the active stack emits QUndoStack::canRedoChanged() + or the active stack changes. + + \a canRedo is the new state, or false if the active stack is 0. + + \sa QUndoStack::canRedoChanged() setActiveStack() +*/ + +/*! \fn void QUndoGroup::undoTextChanged(const QString &undoText) + + This signal is emitted whenever the active stack emits QUndoStack::undoTextChanged() + or the active stack changes. + + \a undoText is the new state, or an empty string if the active stack is 0. + + \sa QUndoStack::undoTextChanged() setActiveStack() +*/ + +/*! \fn void QUndoGroup::redoTextChanged(const QString &redoText) + + This signal is emitted whenever the active stack emits QUndoStack::redoTextChanged() + or the active stack changes. + + \a redoText is the new state, or an empty string if the active stack is 0. + + \sa QUndoStack::redoTextChanged() setActiveStack() +*/ + +QT_END_NAMESPACE + +#endif // QT_NO_UNDOGROUP diff --git a/src/widgets/util/qundogroup.h b/src/widgets/util/qundogroup.h new file mode 100644 index 0000000000..4ed79a9c81 --- /dev/null +++ b/src/widgets/util/qundogroup.h @@ -0,0 +1,110 @@ +/**************************************************************************** +** +** Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies). +** All rights reserved. +** Contact: Nokia Corporation (qt-info@nokia.com) +** +** This file is part of the QtGui module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** No Commercial Usage +** This file contains pre-release code and may not be distributed. +** You may use this file in accordance with the terms and conditions +** contained in the Technology Preview License Agreement accompanying +** this package. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain additional +** rights. These rights are described in the Nokia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** If you have questions regarding the use of this file, please contact +** Nokia at qt-info@nokia.com. +** +** +** +** +** +** +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QUNDOGROUP_H +#define QUNDOGROUP_H + +#include <QtCore/qobject.h> +#include <QtCore/qstring.h> + +QT_BEGIN_HEADER + +QT_BEGIN_NAMESPACE + +class QUndoGroupPrivate; +class QUndoStack; +class QAction; + +QT_MODULE(Gui) + +#ifndef QT_NO_UNDOGROUP + +class Q_GUI_EXPORT QUndoGroup : public QObject +{ + Q_OBJECT + Q_DECLARE_PRIVATE(QUndoGroup) + +public: + explicit QUndoGroup(QObject *parent = 0); + ~QUndoGroup(); + + void addStack(QUndoStack *stack); + void removeStack(QUndoStack *stack); + QList<QUndoStack*> stacks() const; + QUndoStack *activeStack() const; + +#ifndef QT_NO_ACTION + QAction *createUndoAction(QObject *parent, + const QString &prefix = QString()) const; + QAction *createRedoAction(QObject *parent, + const QString &prefix = QString()) const; +#endif // QT_NO_ACTION + bool canUndo() const; + bool canRedo() const; + QString undoText() const; + QString redoText() const; + bool isClean() const; + +public Q_SLOTS: + void undo(); + void redo(); + void setActiveStack(QUndoStack *stack); + +Q_SIGNALS: + void activeStackChanged(QUndoStack *stack); + void indexChanged(int idx); + void cleanChanged(bool clean); + void canUndoChanged(bool canUndo); + void canRedoChanged(bool canRedo); + void undoTextChanged(const QString &undoText); + void redoTextChanged(const QString &redoText); + +private: + Q_DISABLE_COPY(QUndoGroup) +}; + +#endif // QT_NO_UNDOGROUP + +QT_END_NAMESPACE + +QT_END_HEADER + +#endif // QUNDOGROUP_H diff --git a/src/widgets/util/qundostack.cpp b/src/widgets/util/qundostack.cpp new file mode 100644 index 0000000000..6b038ee52e --- /dev/null +++ b/src/widgets/util/qundostack.cpp @@ -0,0 +1,1127 @@ +/**************************************************************************** +** +** Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies). +** All rights reserved. +** Contact: Nokia Corporation (qt-info@nokia.com) +** +** This file is part of the QtGui module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** No Commercial Usage +** This file contains pre-release code and may not be distributed. +** You may use this file in accordance with the terms and conditions +** contained in the Technology Preview License Agreement accompanying +** this package. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain additional +** rights. These rights are described in the Nokia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** If you have questions regarding the use of this file, please contact +** Nokia at qt-info@nokia.com. +** +** +** +** +** +** +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include <QtCore/qdebug.h> +#include "qundostack.h" +#include "qundogroup.h" +#include "qundostack_p.h" + +#ifndef QT_NO_UNDOCOMMAND + +QT_BEGIN_NAMESPACE + +/*! + \class QUndoCommand + \brief The QUndoCommand class is the base class of all commands stored on a QUndoStack. + \since 4.2 + + For an overview of Qt's Undo Framework, see the + \l{Overview of Qt's Undo Framework}{overview document}. + + A QUndoCommand represents a single editing action on a document; for example, + inserting or deleting a block of text in a text editor. QUndoCommand can apply + a change to the document with redo() and undo the change with undo(). The + implementations for these functions must be provided in a derived class. + + \snippet doc/src/snippets/code/src_gui_util_qundostack.cpp 0 + + A QUndoCommand has an associated text(). This is a short string + describing what the command does. It is used to update the text + properties of the stack's undo and redo actions; see + QUndoStack::createUndoAction() and QUndoStack::createRedoAction(). + + QUndoCommand objects are owned by the stack they were pushed on. + QUndoStack deletes a command if it has been undone and a new command is pushed. For example: + +\snippet doc/src/snippets/code/src_gui_util_qundostack.cpp 1 + + In effect, when a command is pushed, it becomes the top-most command + on the stack. + + To support command compression, QUndoCommand has an id() and the virtual function + mergeWith(). These functions are used by QUndoStack::push(). + + To support command macros, a QUndoCommand object can have any number of child + commands. Undoing or redoing the parent command will cause the child + commands to be undone or redone. A command can be assigned + to a parent explicitly in the constructor. In this case, the command + will be owned by the parent. + + The parent in this case is usually an empty command, in that it doesn't + provide its own implementation of undo() and redo(). Instead, it uses + the base implementations of these functions, which simply call undo() or + redo() on all its children. The parent should, however, have a meaningful + text(). + + \snippet doc/src/snippets/code/src_gui_util_qundostack.cpp 2 + + Another way to create macros is to use the convenience functions + QUndoStack::beginMacro() and QUndoStack::endMacro(). + + \sa QUndoStack +*/ + +/*! + Constructs a QUndoCommand object with the given \a parent and \a text. + + If \a parent is not 0, this command is appended to parent's child list. + The parent command then owns this command and will delete it in its + destructor. + + \sa ~QUndoCommand() +*/ + +QUndoCommand::QUndoCommand(const QString &text, QUndoCommand *parent) +{ + d = new QUndoCommandPrivate; + if (parent != 0) + parent->d->child_list.append(this); + d->text = text; +} + +/*! + Constructs a QUndoCommand object with parent \a parent. + + If \a parent is not 0, this command is appended to parent's child list. + The parent command then owns this command and will delete it in its + destructor. + + \sa ~QUndoCommand() +*/ + +QUndoCommand::QUndoCommand(QUndoCommand *parent) +{ + d = new QUndoCommandPrivate; + if (parent != 0) + parent->d->child_list.append(this); +} + +/*! + Destroys the QUndoCommand object and all child commands. + + \sa QUndoCommand() +*/ + +QUndoCommand::~QUndoCommand() +{ + qDeleteAll(d->child_list); + delete d; +} + +/*! + Returns the ID of this command. + + A command ID is used in command compression. It must be an integer unique to + this command's class, or -1 if the command doesn't support compression. + + If the command supports compression this function must be overridden in the + derived class to return the correct ID. The base implementation returns -1. + + QUndoStack::push() will only try to merge two commands if they have the + same ID, and the ID is not -1. + + \sa mergeWith(), QUndoStack::push() +*/ + +int QUndoCommand::id() const +{ + return -1; +} + +/*! + Attempts to merge this command with \a command. Returns true on + success; otherwise returns false. + + If this function returns true, calling this command's redo() must have the same + effect as redoing both this command and \a command. + Similarly, calling this command's undo() must have the same effect as undoing + \a command and this command. + + QUndoStack will only try to merge two commands if they have the same id, and + the id is not -1. + + The default implementation returns false. + + \snippet doc/src/snippets/code/src_gui_util_qundostack.cpp 3 + + \sa id() QUndoStack::push() +*/ + +bool QUndoCommand::mergeWith(const QUndoCommand *command) +{ + Q_UNUSED(command); + return false; +} + +/*! + Applies a change to the document. This function must be implemented in + the derived class. Calling QUndoStack::push(), + QUndoStack::undo() or QUndoStack::redo() from this function leads to + undefined beahavior. + + The default implementation calls redo() on all child commands. + + \sa undo() +*/ + +void QUndoCommand::redo() +{ + for (int i = 0; i < d->child_list.size(); ++i) + d->child_list.at(i)->redo(); +} + +/*! + Reverts a change to the document. After undo() is called, the state of + the document should be the same as before redo() was called. This function must + be implemented in the derived class. Calling QUndoStack::push(), + QUndoStack::undo() or QUndoStack::redo() from this function leads to + undefined beahavior. + + The default implementation calls undo() on all child commands in reverse order. + + \sa redo() +*/ + +void QUndoCommand::undo() +{ + for (int i = d->child_list.size() - 1; i >= 0; --i) + d->child_list.at(i)->undo(); +} + +/*! + Returns a short text string describing what this command does; for example, + "insert text". + + The text is used when the text properties of the stack's undo and redo + actions are updated. + + \sa setText(), QUndoStack::createUndoAction(), QUndoStack::createRedoAction() +*/ + +QString QUndoCommand::text() const +{ + return d->text; +} + +/*! + Sets the command's text to be the \a text specified. + + The specified text should be a short user-readable string describing what this + command does. + + \sa text() QUndoStack::createUndoAction() QUndoStack::createRedoAction() +*/ + +void QUndoCommand::setText(const QString &text) +{ + d->text = text; +} + +/*! + \since 4.4 + + Returns the number of child commands in this command. + + \sa child() +*/ + +int QUndoCommand::childCount() const +{ + return d->child_list.count(); +} + +/*! + \since 4.4 + + Returns the child command at \a index. + + \sa childCount(), QUndoStack::command() +*/ + +const QUndoCommand *QUndoCommand::child(int index) const +{ + if (index < 0 || index >= d->child_list.count()) + return 0; + return d->child_list.at(index); +} + +#endif // QT_NO_UNDOCOMMAND + +#ifndef QT_NO_UNDOSTACK + +/*! + \class QUndoStack + \brief The QUndoStack class is a stack of QUndoCommand objects. + \since 4.2 + + For an overview of Qt's Undo Framework, see the + \l{Overview of Qt's Undo Framework}{overview document}. + + An undo stack maintains a stack of commands that have been applied to a + document. + + New commands are pushed on the stack using push(). Commands can be + undone and redone using undo() and redo(), or by triggering the + actions returned by createUndoAction() and createRedoAction(). + + QUndoStack keeps track of the \a current command. This is the command + which will be executed by the next call to redo(). The index of this + command is returned by index(). The state of the edited object can be + rolled forward or back using setIndex(). If the top-most command on the + stack has already been redone, index() is equal to count(). + + QUndoStack provides support for undo and redo actions, command + compression, command macros, and supports the concept of a + \e{clean state}. + + \section1 Undo and Redo Actions + + QUndoStack provides convenient undo and redo QAction objects, which + can be inserted into a menu or a toolbar. When commands are undone or + redone, QUndoStack updates the text properties of these actions + to reflect what change they will trigger. The actions are also disabled + when no command is available for undo or redo. These actions + are returned by QUndoStack::createUndoAction() and QUndoStack::createRedoAction(). + + \section1 Command Compression and Macros + + Command compression is useful when several commands can be compressed + into a single command that can be undone and redone in a single operation. + For example, when a user types a character in a text editor, a new command + is created. This command inserts the character into the document at the + cursor position. However, it is more convenient for the user to be able + to undo or redo typing of whole words, sentences, or paragraphs. + Command compression allows these single-character commands to be merged + into a single command which inserts or deletes sections of text. + For more information, see QUndoCommand::mergeWith() and push(). + + A command macro is a sequence of commands, all of which are undone and + redone in one go. Command macros are created by giving a command a list + of child commands. + Undoing or redoing the parent command will cause the child commands to + be undone or redone. Command macros may be created explicitly + by specifying a parent in the QUndoCommand constructor, or by using the + convenience functions beginMacro() and endMacro(). + + Although command compression and macros appear to have the same effect to the + user, they often have different uses in an application. Commands that + perform small changes to a document may be usefully compressed if there is + no need to individually record them, and if only larger changes are relevant + to the user. + However, for commands that need to be recorded individually, or those that + cannot be compressed, it is useful to use macros to provide a more convenient + user experience while maintaining a record of each command. + + \section1 Clean State + + QUndoStack supports the concept of a clean state. When the + document is saved to disk, the stack can be marked as clean using + setClean(). Whenever the stack returns to this state through undoing and + redoing commands, it emits the signal cleanChanged(). This signal + is also emitted when the stack leaves the clean state. This signal is + usually used to enable and disable the save actions in the application, + and to update the document's title to reflect that it contains unsaved + changes. + + \sa QUndoCommand, QUndoView +*/ + +#ifndef QT_NO_ACTION + +QUndoAction::QUndoAction(const QString &prefix, QObject *parent) + : QAction(parent) +{ + m_prefix = prefix; +} + +void QUndoAction::setPrefixedText(const QString &text) +{ + QString s = m_prefix; + if (!m_prefix.isEmpty() && !text.isEmpty()) + s.append(QLatin1Char(' ')); + s.append(text); + setText(s); +} + +#endif // QT_NO_ACTION + +/*! \internal + Sets the current index to \a idx, emitting appropriate signals. If \a clean is true, + makes \a idx the clean index as well. +*/ + +void QUndoStackPrivate::setIndex(int idx, bool clean) +{ + Q_Q(QUndoStack); + + bool was_clean = index == clean_index; + + if (idx != index) { + index = idx; + emit q->indexChanged(index); + emit q->canUndoChanged(q->canUndo()); + emit q->undoTextChanged(q->undoText()); + emit q->canRedoChanged(q->canRedo()); + emit q->redoTextChanged(q->redoText()); + } + + if (clean) + clean_index = index; + + bool is_clean = index == clean_index; + if (is_clean != was_clean) + emit q->cleanChanged(is_clean); +} + +/*! \internal + If the number of commands on the stack exceedes the undo limit, deletes commands from + the bottom of the stack. + + Returns true if commands were deleted. +*/ + +bool QUndoStackPrivate::checkUndoLimit() +{ + if (undo_limit <= 0 || !macro_stack.isEmpty() || undo_limit >= command_list.count()) + return false; + + int del_count = command_list.count() - undo_limit; + + for (int i = 0; i < del_count; ++i) + delete command_list.takeFirst(); + + index -= del_count; + if (clean_index != -1) { + if (clean_index < del_count) + clean_index = -1; // we've deleted the clean command + else + clean_index -= del_count; + } + + return true; +} + +/*! + Constructs an empty undo stack with the parent \a parent. The + stack will initially be in the clean state. If \a parent is a + QUndoGroup object, the stack is automatically added to the group. + + \sa push() +*/ + +QUndoStack::QUndoStack(QObject *parent) + : QObject(*(new QUndoStackPrivate), parent) +{ +#ifndef QT_NO_UNDOGROUP + if (QUndoGroup *group = qobject_cast<QUndoGroup*>(parent)) + group->addStack(this); +#endif +} + +/*! + Destroys the undo stack, deleting any commands that are on it. If the + stack is in a QUndoGroup, the stack is automatically removed from the group. + + \sa QUndoStack() +*/ + +QUndoStack::~QUndoStack() +{ +#ifndef QT_NO_UNDOGROUP + Q_D(QUndoStack); + if (d->group != 0) + d->group->removeStack(this); +#endif + clear(); +} + +/*! + Clears the command stack by deleting all commands on it, and returns the stack + to the clean state. + + Commands are not undone or redone; the state of the edited object remains + unchanged. + + This function is usually used when the contents of the document are + abandoned. + + \sa QUndoStack() +*/ + +void QUndoStack::clear() +{ + Q_D(QUndoStack); + + if (d->command_list.isEmpty()) + return; + + bool was_clean = isClean(); + + d->macro_stack.clear(); + qDeleteAll(d->command_list); + d->command_list.clear(); + + d->index = 0; + d->clean_index = 0; + + emit indexChanged(0); + emit canUndoChanged(false); + emit undoTextChanged(QString()); + emit canRedoChanged(false); + emit redoTextChanged(QString()); + + if (!was_clean) + emit cleanChanged(true); +} + +/*! + Pushes \a cmd on the stack or merges it with the most recently executed command. + In either case, executes \a cmd by calling its redo() function. + + If \a cmd's id is not -1, and if the id is the same as that of the + most recently executed command, QUndoStack will attempt to merge the two + commands by calling QUndoCommand::mergeWith() on the most recently executed + command. If QUndoCommand::mergeWith() returns true, \a cmd is deleted. + + In all other cases \a cmd is simply pushed on the stack. + + If commands were undone before \a cmd was pushed, the current command and + all commands above it are deleted. Hence \a cmd always ends up being the + top-most on the stack. + + Once a command is pushed, the stack takes ownership of it. There + are no getters to return the command, since modifying it after it has + been executed will almost always lead to corruption of the document's + state. + + \sa QUndoCommand::id() QUndoCommand::mergeWith() +*/ + +void QUndoStack::push(QUndoCommand *cmd) +{ + Q_D(QUndoStack); + cmd->redo(); + + bool macro = !d->macro_stack.isEmpty(); + + QUndoCommand *cur = 0; + if (macro) { + QUndoCommand *macro_cmd = d->macro_stack.last(); + if (!macro_cmd->d->child_list.isEmpty()) + cur = macro_cmd->d->child_list.last(); + } else { + if (d->index > 0) + cur = d->command_list.at(d->index - 1); + while (d->index < d->command_list.size()) + delete d->command_list.takeLast(); + if (d->clean_index > d->index) + d->clean_index = -1; // we've deleted the clean state + } + + bool try_merge = cur != 0 + && cur->id() != -1 + && cur->id() == cmd->id() + && (macro || d->index != d->clean_index); + + if (try_merge && cur->mergeWith(cmd)) { + delete cmd; + if (!macro) { + emit indexChanged(d->index); + emit canUndoChanged(canUndo()); + emit undoTextChanged(undoText()); + emit canRedoChanged(canRedo()); + emit redoTextChanged(redoText()); + } + } else { + if (macro) { + d->macro_stack.last()->d->child_list.append(cmd); + } else { + d->command_list.append(cmd); + d->checkUndoLimit(); + d->setIndex(d->index + 1, false); + } + } +} + +/*! + Marks the stack as clean and emits cleanChanged() if the stack was + not already clean. + + Whenever the stack returns to this state through the use of undo/redo + commands, it emits the signal cleanChanged(). This signal is also + emitted when the stack leaves the clean state. + + \sa isClean(), cleanIndex() +*/ + +void QUndoStack::setClean() +{ + Q_D(QUndoStack); + if (!d->macro_stack.isEmpty()) { + qWarning("QUndoStack::setClean(): cannot set clean in the middle of a macro"); + return; + } + + d->setIndex(d->index, true); +} + +/*! + If the stack is in the clean state, returns true; otherwise returns false. + + \sa setClean() cleanIndex() +*/ + +bool QUndoStack::isClean() const +{ + Q_D(const QUndoStack); + if (!d->macro_stack.isEmpty()) + return false; + return d->clean_index == d->index; +} + +/*! + Returns the clean index. This is the index at which setClean() was called. + + A stack may not have a clean index. This happens if a document is saved, + some commands are undone, then a new command is pushed. Since + push() deletes all the undone commands before pushing the new command, the stack + can't return to the clean state again. In this case, this function returns -1. + + \sa isClean() setClean() +*/ + +int QUndoStack::cleanIndex() const +{ + Q_D(const QUndoStack); + return d->clean_index; +} + +/*! + Undoes the command below the current command by calling QUndoCommand::undo(). + Decrements the current command index. + + If the stack is empty, or if the bottom command on the stack has already been + undone, this function does nothing. + + \sa redo() index() +*/ + +void QUndoStack::undo() +{ + Q_D(QUndoStack); + if (d->index == 0) + return; + + if (!d->macro_stack.isEmpty()) { + qWarning("QUndoStack::undo(): cannot undo in the middle of a macro"); + return; + } + + int idx = d->index - 1; + d->command_list.at(idx)->undo(); + d->setIndex(idx, false); +} + +/*! + Redoes the current command by calling QUndoCommand::redo(). Increments the current + command index. + + If the stack is empty, or if the top command on the stack has already been + redone, this function does nothing. + + \sa undo() index() +*/ + +void QUndoStack::redo() +{ + Q_D(QUndoStack); + if (d->index == d->command_list.size()) + return; + + if (!d->macro_stack.isEmpty()) { + qWarning("QUndoStack::redo(): cannot redo in the middle of a macro"); + return; + } + + d->command_list.at(d->index)->redo(); + d->setIndex(d->index + 1, false); +} + +/*! + Returns the number of commands on the stack. Macro commands are counted as + one command. + + \sa index() setIndex() command() +*/ + +int QUndoStack::count() const +{ + Q_D(const QUndoStack); + return d->command_list.size(); +} + +/*! + Returns the index of the current command. This is the command that will be + executed on the next call to redo(). It is not always the top-most command + on the stack, since a number of commands may have been undone. + + \sa undo() redo() count() +*/ + +int QUndoStack::index() const +{ + Q_D(const QUndoStack); + return d->index; +} + +/*! + Repeatedly calls undo() or redo() until the current command index reaches + \a idx. This function can be used to roll the state of the document forwards + of backwards. indexChanged() is emitted only once. + + \sa index() count() undo() redo() +*/ + +void QUndoStack::setIndex(int idx) +{ + Q_D(QUndoStack); + if (!d->macro_stack.isEmpty()) { + qWarning("QUndoStack::setIndex(): cannot set index in the middle of a macro"); + return; + } + + if (idx < 0) + idx = 0; + else if (idx > d->command_list.size()) + idx = d->command_list.size(); + + int i = d->index; + while (i < idx) + d->command_list.at(i++)->redo(); + while (i > idx) + d->command_list.at(--i)->undo(); + + d->setIndex(idx, false); +} + +/*! + Returns true if there is a command available for undo; otherwise returns false. + + This function returns false if the stack is empty, or if the bottom command + on the stack has already been undone. + + Synonymous with index() == 0. + + \sa index() canRedo() +*/ + +bool QUndoStack::canUndo() const +{ + Q_D(const QUndoStack); + if (!d->macro_stack.isEmpty()) + return false; + return d->index > 0; +} + +/*! + Returns true if there is a command available for redo; otherwise returns false. + + This function returns false if the stack is empty or if the top command + on the stack has already been redone. + + Synonymous with index() == count(). + + \sa index() canUndo() +*/ + +bool QUndoStack::canRedo() const +{ + Q_D(const QUndoStack); + if (!d->macro_stack.isEmpty()) + return false; + return d->index < d->command_list.size(); +} + +/*! + Returns the text of the command which will be undone in the next call to undo(). + + \sa QUndoCommand::text() redoText() +*/ + +QString QUndoStack::undoText() const +{ + Q_D(const QUndoStack); + if (!d->macro_stack.isEmpty()) + return QString(); + if (d->index > 0) + return d->command_list.at(d->index - 1)->text(); + return QString(); +} + +/*! + Returns the text of the command which will be redone in the next call to redo(). + + \sa QUndoCommand::text() undoText() +*/ + +QString QUndoStack::redoText() const +{ + Q_D(const QUndoStack); + if (!d->macro_stack.isEmpty()) + return QString(); + if (d->index < d->command_list.size()) + return d->command_list.at(d->index)->text(); + return QString(); +} + +#ifndef QT_NO_ACTION + +/*! + Creates an undo QAction object with the given \a parent. + + Triggering this action will cause a call to undo(). The text of this action + is the text of the command which will be undone in the next call to undo(), + prefixed by the specified \a prefix. If there is no command available for undo, + this action will be disabled. + + If \a prefix is empty, the default prefix "Undo" is used. + + \sa createRedoAction(), canUndo(), QUndoCommand::text() +*/ + +QAction *QUndoStack::createUndoAction(QObject *parent, const QString &prefix) const +{ + QString pref = prefix.isEmpty() ? tr("Undo") : prefix; + QUndoAction *result = new QUndoAction(pref, parent); + result->setEnabled(canUndo()); + result->setPrefixedText(undoText()); + connect(this, SIGNAL(canUndoChanged(bool)), + result, SLOT(setEnabled(bool))); + connect(this, SIGNAL(undoTextChanged(QString)), + result, SLOT(setPrefixedText(QString))); + connect(result, SIGNAL(triggered()), this, SLOT(undo())); + return result; +} + +/*! + Creates an redo QAction object with the given \a parent. + + Triggering this action will cause a call to redo(). The text of this action + is the text of the command which will be redone in the next call to redo(), + prefixed by the specified \a prefix. If there is no command available for redo, + this action will be disabled. + + If \a prefix is empty, the default prefix "Redo" is used. + + \sa createUndoAction(), canRedo(), QUndoCommand::text() +*/ + +QAction *QUndoStack::createRedoAction(QObject *parent, const QString &prefix) const +{ + QString pref = prefix.isEmpty() ? tr("Redo") : prefix; + QUndoAction *result = new QUndoAction(pref, parent); + result->setEnabled(canRedo()); + result->setPrefixedText(redoText()); + connect(this, SIGNAL(canRedoChanged(bool)), + result, SLOT(setEnabled(bool))); + connect(this, SIGNAL(redoTextChanged(QString)), + result, SLOT(setPrefixedText(QString))); + connect(result, SIGNAL(triggered()), this, SLOT(redo())); + return result; +} + +#endif // QT_NO_ACTION + +/*! + Begins composition of a macro command with the given \a text description. + + An empty command described by the specified \a text is pushed on the stack. + Any subsequent commands pushed on the stack will be appended to the empty + command's children until endMacro() is called. + + Calls to beginMacro() and endMacro() may be nested, but every call to + beginMacro() must have a matching call to endMacro(). + + While a macro is composed, the stack is disabled. This means that: + \list + \i indexChanged() and cleanChanged() are not emitted, + \i canUndo() and canRedo() return false, + \i calling undo() or redo() has no effect, + \i the undo/redo actions are disabled. + \endlist + + The stack becomes enabled and appropriate signals are emitted when endMacro() + is called for the outermost macro. + + \snippet doc/src/snippets/code/src_gui_util_qundostack.cpp 4 + + This code is equivalent to: + + \snippet doc/src/snippets/code/src_gui_util_qundostack.cpp 5 + + \sa endMacro() +*/ + +void QUndoStack::beginMacro(const QString &text) +{ + Q_D(QUndoStack); + QUndoCommand *cmd = new QUndoCommand(); + cmd->setText(text); + + if (d->macro_stack.isEmpty()) { + while (d->index < d->command_list.size()) + delete d->command_list.takeLast(); + if (d->clean_index > d->index) + d->clean_index = -1; // we've deleted the clean state + d->command_list.append(cmd); + } else { + d->macro_stack.last()->d->child_list.append(cmd); + } + d->macro_stack.append(cmd); + + if (d->macro_stack.count() == 1) { + emit canUndoChanged(false); + emit undoTextChanged(QString()); + emit canRedoChanged(false); + emit redoTextChanged(QString()); + } +} + +/*! + Ends composition of a macro command. + + If this is the outermost macro in a set nested macros, this function emits + indexChanged() once for the entire macro command. + + \sa beginMacro() +*/ + +void QUndoStack::endMacro() +{ + Q_D(QUndoStack); + if (d->macro_stack.isEmpty()) { + qWarning("QUndoStack::endMacro(): no matching beginMacro()"); + return; + } + + d->macro_stack.removeLast(); + + if (d->macro_stack.isEmpty()) { + d->checkUndoLimit(); + d->setIndex(d->index + 1, false); + } +} + +/*! + \since 4.4 + + Returns a const pointer to the command at \a index. + + This function returns a const pointer, because modifying a command, + once it has been pushed onto the stack and executed, almost always + causes corruption of the state of the document, if the command is + later undone or redone. + + \sa QUndoCommand::child() +*/ +const QUndoCommand *QUndoStack::command(int index) const +{ + Q_D(const QUndoStack); + + if (index < 0 || index >= d->command_list.count()) + return 0; + return d->command_list.at(index); +} + +/*! + Returns the text of the command at index \a idx. + + \sa beginMacro() +*/ + +QString QUndoStack::text(int idx) const +{ + Q_D(const QUndoStack); + + if (idx < 0 || idx >= d->command_list.size()) + return QString(); + return d->command_list.at(idx)->text(); +} + +/*! + \property QUndoStack::undoLimit + \brief the maximum number of commands on this stack. + \since 4.3 + + When the number of commands on a stack exceedes the stack's undoLimit, commands are + deleted from the bottom of the stack. Macro commands (commands with child commands) + are treated as one command. The default value is 0, which means that there is no + limit. + + This property may only be set when the undo stack is empty, since setting it on a + non-empty stack might delete the command at the current index. Calling setUndoLimit() + on a non-empty stack prints a warning and does nothing. +*/ + +void QUndoStack::setUndoLimit(int limit) +{ + Q_D(QUndoStack); + + if (!d->command_list.isEmpty()) { + qWarning("QUndoStack::setUndoLimit(): an undo limit can only be set when the stack is empty"); + return; + } + + if (limit == d->undo_limit) + return; + d->undo_limit = limit; + d->checkUndoLimit(); +} + +int QUndoStack::undoLimit() const +{ + Q_D(const QUndoStack); + + return d->undo_limit; +} + +/*! + \property QUndoStack::active + \brief the active status of this stack. + + An application often has multiple undo stacks, one for each opened document. The active + stack is the one associated with the currently active document. If the stack belongs + to a QUndoGroup, calls to QUndoGroup::undo() or QUndoGroup::redo() will be forwarded + to this stack when it is active. If the QUndoGroup is watched by a QUndoView, the view + will display the contents of this stack when it is active. If the stack does not belong to + a QUndoGroup, making it active has no effect. + + It is the programmer's responsibility to specify which stack is active by + calling setActive(), usually when the associated document window receives focus. + + \sa QUndoGroup +*/ + +void QUndoStack::setActive(bool active) +{ +#ifdef QT_NO_UNDOGROUP + Q_UNUSED(active); +#else + Q_D(QUndoStack); + + if (d->group != 0) { + if (active) + d->group->setActiveStack(this); + else if (d->group->activeStack() == this) + d->group->setActiveStack(0); + } +#endif +} + +bool QUndoStack::isActive() const +{ +#ifdef QT_NO_UNDOGROUP + return true; +#else + Q_D(const QUndoStack); + return d->group == 0 || d->group->activeStack() == this; +#endif +} + +/*! + \fn void QUndoStack::indexChanged(int idx) + + This signal is emitted whenever a command modifies the state of the document. + This happens when a command is undone or redone. When a macro + command is undone or redone, or setIndex() is called, this signal + is emitted only once. + + \a idx specifies the index of the current command, ie. the command which will be + executed on the next call to redo(). + + \sa index() setIndex() +*/ + +/*! + \fn void QUndoStack::cleanChanged(bool clean) + + This signal is emitted whenever the stack enters or leaves the clean state. + If \a clean is true, the stack is in a clean state; otherwise this signal + indicates that it has left the clean state. + + \sa isClean() setClean() +*/ + +/*! + \fn void QUndoStack::undoTextChanged(const QString &undoText) + + This signal is emitted whenever the value of undoText() changes. It is + used to update the text property of the undo action returned by createUndoAction(). + \a undoText specifies the new text. +*/ + +/*! + \fn void QUndoStack::canUndoChanged(bool canUndo) + + This signal is emitted whenever the value of canUndo() changes. It is + used to enable or disable the undo action returned by createUndoAction(). + \a canUndo specifies the new value. +*/ + +/*! + \fn void QUndoStack::redoTextChanged(const QString &redoText) + + This signal is emitted whenever the value of redoText() changes. It is + used to update the text property of the redo action returned by createRedoAction(). + \a redoText specifies the new text. +*/ + +/*! + \fn void QUndoStack::canRedoChanged(bool canRedo) + + This signal is emitted whenever the value of canRedo() changes. It is + used to enable or disable the redo action returned by createRedoAction(). + \a canRedo specifies the new value. +*/ + +QT_END_NAMESPACE + +#endif // QT_NO_UNDOSTACK diff --git a/src/widgets/util/qundostack.h b/src/widgets/util/qundostack.h new file mode 100644 index 0000000000..65941b53c3 --- /dev/null +++ b/src/widgets/util/qundostack.h @@ -0,0 +1,158 @@ +/**************************************************************************** +** +** Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies). +** All rights reserved. +** Contact: Nokia Corporation (qt-info@nokia.com) +** +** This file is part of the QtGui module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** No Commercial Usage +** This file contains pre-release code and may not be distributed. +** You may use this file in accordance with the terms and conditions +** contained in the Technology Preview License Agreement accompanying +** this package. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain additional +** rights. These rights are described in the Nokia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** If you have questions regarding the use of this file, please contact +** Nokia at qt-info@nokia.com. +** +** +** +** +** +** +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QUNDOSTACK_H +#define QUNDOSTACK_H + +#include <QtCore/qobject.h> +#include <QtCore/qstring.h> + +QT_BEGIN_HEADER + +QT_BEGIN_NAMESPACE + +QT_MODULE(Gui) + +class QAction; +class QUndoCommandPrivate; +class QUndoStackPrivate; + +#ifndef QT_NO_UNDOCOMMAND + +class Q_GUI_EXPORT QUndoCommand +{ + QUndoCommandPrivate *d; + +public: + explicit QUndoCommand(QUndoCommand *parent = 0); + explicit QUndoCommand(const QString &text, QUndoCommand *parent = 0); + virtual ~QUndoCommand(); + + virtual void undo(); + virtual void redo(); + + QString text() const; + void setText(const QString &text); + + virtual int id() const; + virtual bool mergeWith(const QUndoCommand *other); + + int childCount() const; + const QUndoCommand *child(int index) const; + +private: + Q_DISABLE_COPY(QUndoCommand) + friend class QUndoStack; +}; + +#endif // QT_NO_UNDOCOMMAND + +#ifndef QT_NO_UNDOSTACK + +class Q_GUI_EXPORT QUndoStack : public QObject +{ + Q_OBJECT + Q_DECLARE_PRIVATE(QUndoStack) + Q_PROPERTY(bool active READ isActive WRITE setActive) + Q_PROPERTY(int undoLimit READ undoLimit WRITE setUndoLimit) + +public: + explicit QUndoStack(QObject *parent = 0); + ~QUndoStack(); + void clear(); + + void push(QUndoCommand *cmd); + + bool canUndo() const; + bool canRedo() const; + QString undoText() const; + QString redoText() const; + + int count() const; + int index() const; + QString text(int idx) const; + +#ifndef QT_NO_ACTION + QAction *createUndoAction(QObject *parent, + const QString &prefix = QString()) const; + QAction *createRedoAction(QObject *parent, + const QString &prefix = QString()) const; +#endif // QT_NO_ACTION + + bool isActive() const; + bool isClean() const; + int cleanIndex() const; + + void beginMacro(const QString &text); + void endMacro(); + + void setUndoLimit(int limit); + int undoLimit() const; + + const QUndoCommand *command(int index) const; + +public Q_SLOTS: + void setClean(); + void setIndex(int idx); + void undo(); + void redo(); + void setActive(bool active = true); + +Q_SIGNALS: + void indexChanged(int idx); + void cleanChanged(bool clean); + void canUndoChanged(bool canUndo); + void canRedoChanged(bool canRedo); + void undoTextChanged(const QString &undoText); + void redoTextChanged(const QString &redoText); + +private: + Q_DISABLE_COPY(QUndoStack) + friend class QUndoGroup; +}; + +#endif // QT_NO_UNDOSTACK + +QT_END_NAMESPACE + +QT_END_HEADER + +#endif // QUNDOSTACK_H diff --git a/src/widgets/util/qundostack_p.h b/src/widgets/util/qundostack_p.h new file mode 100644 index 0000000000..3c7d0e7d85 --- /dev/null +++ b/src/widgets/util/qundostack_p.h @@ -0,0 +1,111 @@ +/**************************************************************************** +** +** Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies). +** All rights reserved. +** Contact: Nokia Corporation (qt-info@nokia.com) +** +** This file is part of the QtGui module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** No Commercial Usage +** This file contains pre-release code and may not be distributed. +** You may use this file in accordance with the terms and conditions +** contained in the Technology Preview License Agreement accompanying +** this package. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain additional +** rights. These rights are described in the Nokia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** If you have questions regarding the use of this file, please contact +** Nokia at qt-info@nokia.com. +** +** +** +** +** +** +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QUNDOSTACK_P_H +#define QUNDOSTACK_P_H + +#include <private/qobject_p.h> +#include <QtCore/qlist.h> +#include <QtCore/qstring.h> +#include <QtGui/qaction.h> + +#include "qundostack.h" + +QT_BEGIN_NAMESPACE +class QUndoCommand; +class QUndoGroup; + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists for the convenience +// of qapplication_*.cpp, qwidget*.cpp and qfiledialog.cpp. This header +// file may change from version to version without notice, or even be removed. +// +// We mean it. +// + +class QUndoCommandPrivate +{ +public: + QUndoCommandPrivate() : id(-1) {} + QList<QUndoCommand*> child_list; + QString text; + int id; +}; + +#ifndef QT_NO_UNDOSTACK + +class QUndoStackPrivate : public QObjectPrivate +{ + Q_DECLARE_PUBLIC(QUndoStack) +public: + QUndoStackPrivate() : index(0), clean_index(0), group(0), undo_limit(0) {} + + QList<QUndoCommand*> command_list; + QList<QUndoCommand*> macro_stack; + int index; + int clean_index; + QUndoGroup *group; + int undo_limit; + + void setIndex(int idx, bool clean); + bool checkUndoLimit(); +}; + +#ifndef QT_NO_ACTION +class QUndoAction : public QAction +{ + Q_OBJECT +public: + QUndoAction(const QString &prefix, QObject *parent = 0); +public Q_SLOTS: + void setPrefixedText(const QString &text); +private: + QString m_prefix; +}; +#endif // QT_NO_ACTION + + +QT_END_NAMESPACE +#endif // QT_NO_UNDOSTACK +#endif // QUNDOSTACK_P_H diff --git a/src/widgets/util/qundoview.cpp b/src/widgets/util/qundoview.cpp new file mode 100644 index 0000000000..43c1774b25 --- /dev/null +++ b/src/widgets/util/qundoview.cpp @@ -0,0 +1,476 @@ +/**************************************************************************** +** +** Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies). +** All rights reserved. +** Contact: Nokia Corporation (qt-info@nokia.com) +** +** This file is part of the QtGui module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** No Commercial Usage +** This file contains pre-release code and may not be distributed. +** You may use this file in accordance with the terms and conditions +** contained in the Technology Preview License Agreement accompanying +** this package. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain additional +** rights. These rights are described in the Nokia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** If you have questions regarding the use of this file, please contact +** Nokia at qt-info@nokia.com. +** +** +** +** +** +** +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qundostack.h" +#include "qundoview.h" + +#ifndef QT_NO_UNDOVIEW + +#include "qundogroup.h" +#include <QtCore/qabstractitemmodel.h> +#include <QtCore/qpointer.h> +#include <QtGui/qicon.h> +#include <private/qlistview_p.h> + +QT_BEGIN_NAMESPACE + +class QUndoModel : public QAbstractItemModel +{ + Q_OBJECT +public: + QUndoModel(QObject *parent = 0); + + QUndoStack *stack() const; + + virtual QModelIndex index(int row, int column, + const QModelIndex &parent = QModelIndex()) const; + virtual QModelIndex parent(const QModelIndex &child) const; + virtual int rowCount(const QModelIndex &parent = QModelIndex()) const; + virtual int columnCount(const QModelIndex &parent = QModelIndex()) const; + virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const; + + QModelIndex selectedIndex() const; + QItemSelectionModel *selectionModel() const; + + QString emptyLabel() const; + void setEmptyLabel(const QString &label); + + void setCleanIcon(const QIcon &icon); + QIcon cleanIcon() const; + +public slots: + void setStack(QUndoStack *stack); + +private slots: + void stackChanged(); + void stackDestroyed(QObject *obj); + void setStackCurrentIndex(const QModelIndex &index); + +private: + QUndoStack *m_stack; + QItemSelectionModel *m_sel_model; + QString m_emty_label; + QIcon m_clean_icon; +}; + +QUndoModel::QUndoModel(QObject *parent) + : QAbstractItemModel(parent) +{ + m_stack = 0; + m_sel_model = new QItemSelectionModel(this, this); + connect(m_sel_model, SIGNAL(currentChanged(QModelIndex,QModelIndex)), + this, SLOT(setStackCurrentIndex(QModelIndex))); + m_emty_label = tr("<empty>"); +} + +QItemSelectionModel *QUndoModel::selectionModel() const +{ + return m_sel_model; +} + +QUndoStack *QUndoModel::stack() const +{ + return m_stack; +} + +void QUndoModel::setStack(QUndoStack *stack) +{ + if (m_stack == stack) + return; + + if (m_stack != 0) { + disconnect(m_stack, SIGNAL(cleanChanged(bool)), this, SLOT(stackChanged())); + disconnect(m_stack, SIGNAL(indexChanged(int)), this, SLOT(stackChanged())); + disconnect(m_stack, SIGNAL(destroyed(QObject*)), this, SLOT(stackDestroyed(QObject*))); + } + m_stack = stack; + if (m_stack != 0) { + connect(m_stack, SIGNAL(cleanChanged(bool)), this, SLOT(stackChanged())); + connect(m_stack, SIGNAL(indexChanged(int)), this, SLOT(stackChanged())); + connect(m_stack, SIGNAL(destroyed(QObject*)), this, SLOT(stackDestroyed(QObject*))); + } + + stackChanged(); +} + +void QUndoModel::stackDestroyed(QObject *obj) +{ + if (obj != m_stack) + return; + m_stack = 0; + + stackChanged(); +} + +void QUndoModel::stackChanged() +{ + reset(); + m_sel_model->setCurrentIndex(selectedIndex(), QItemSelectionModel::ClearAndSelect); +} + +void QUndoModel::setStackCurrentIndex(const QModelIndex &index) +{ + if (m_stack == 0) + return; + + if (index == selectedIndex()) + return; + + if (index.column() != 0) + return; + + m_stack->setIndex(index.row()); +} + +QModelIndex QUndoModel::selectedIndex() const +{ + return m_stack == 0 ? QModelIndex() : createIndex(m_stack->index(), 0); +} + +QModelIndex QUndoModel::index(int row, int column, const QModelIndex &parent) const +{ + if (m_stack == 0) + return QModelIndex(); + + if (parent.isValid()) + return QModelIndex(); + + if (column != 0) + return QModelIndex(); + + if (row < 0 || row > m_stack->count()) + return QModelIndex(); + + return createIndex(row, column); +} + +QModelIndex QUndoModel::parent(const QModelIndex&) const +{ + return QModelIndex(); +} + +int QUndoModel::rowCount(const QModelIndex &parent) const +{ + if (m_stack == 0) + return 0; + + if (parent.isValid()) + return 0; + + return m_stack->count() + 1; +} + +int QUndoModel::columnCount(const QModelIndex&) const +{ + return 1; +} + +QVariant QUndoModel::data(const QModelIndex &index, int role) const +{ + if (m_stack == 0) + return QVariant(); + + if (index.column() != 0) + return QVariant(); + + if (index.row() < 0 || index.row() > m_stack->count()) + return QVariant(); + + if (role == Qt::DisplayRole) { + if (index.row() == 0) + return m_emty_label; + return m_stack->text(index.row() - 1); + } else if (role == Qt::DecorationRole) { + if (index.row() == m_stack->cleanIndex() && !m_clean_icon.isNull()) + return m_clean_icon; + return QVariant(); + } + + return QVariant(); +} + +QString QUndoModel::emptyLabel() const +{ + return m_emty_label; +} + +void QUndoModel::setEmptyLabel(const QString &label) +{ + m_emty_label = label; + stackChanged(); +} + +void QUndoModel::setCleanIcon(const QIcon &icon) +{ + m_clean_icon = icon; + stackChanged(); +} + +QIcon QUndoModel::cleanIcon() const +{ + return m_clean_icon; +} + +/*! + \class QUndoView + \brief The QUndoView class displays the contents of a QUndoStack. + \since 4.2 + + \ingroup advanced + + QUndoView is a QListView which displays the list of commands pushed on an undo stack. + The most recently executed command is always selected. Selecting a different command + results in a call to QUndoStack::setIndex(), rolling the state of the document + backwards or forward to the new command. + + The stack can be set explicitly with setStack(). Alternatively, a QUndoGroup object can + be set with setGroup(). The view will then update itself automatically whenever the + active stack of the group changes. + + \image qundoview.png +*/ + +class QUndoViewPrivate : public QListViewPrivate +{ + Q_DECLARE_PUBLIC(QUndoView) +public: + QUndoViewPrivate() : +#ifndef QT_NO_UNDOGROUP + group(0), +#endif + model(0) {} + +#ifndef QT_NO_UNDOGROUP + QPointer<QUndoGroup> group; +#endif + QUndoModel *model; + + void init(); +}; + +void QUndoViewPrivate::init() +{ + Q_Q(QUndoView); + + model = new QUndoModel(q); + q->setModel(model); + q->setSelectionModel(model->selectionModel()); +} + +/*! + Constructs a new view with parent \a parent. +*/ + +QUndoView::QUndoView(QWidget *parent) + : QListView(*new QUndoViewPrivate(), parent) +{ + Q_D(QUndoView); + d->init(); +} + +/*! + Constructs a new view with parent \a parent and sets the observed stack to \a stack. +*/ + +QUndoView::QUndoView(QUndoStack *stack, QWidget *parent) + : QListView(*new QUndoViewPrivate(), parent) +{ + Q_D(QUndoView); + d->init(); + setStack(stack); +} + +#ifndef QT_NO_UNDOGROUP + +/*! + Constructs a new view with parent \a parent and sets the observed group to \a group. + + The view will update itself autmiatically whenever the active stack of the group changes. +*/ + +QUndoView::QUndoView(QUndoGroup *group, QWidget *parent) + : QListView(*new QUndoViewPrivate(), parent) +{ + Q_D(QUndoView); + d->init(); + setGroup(group); +} + +#endif // QT_NO_UNDOGROUP + +/*! + Destroys this view. +*/ + +QUndoView::~QUndoView() +{ +} + +/*! + Returns the stack currently displayed by this view. If the view is looking at a + QUndoGroup, this the group's active stack. + + \sa setStack() setGroup() +*/ + +QUndoStack *QUndoView::stack() const +{ + Q_D(const QUndoView); + return d->model->stack(); +} + +/*! + Sets the stack displayed by this view to \a stack. If \a stack is 0, the view + will be empty. + + If the view was previously looking at a QUndoGroup, the group is set to 0. + + \sa stack() setGroup() +*/ + +void QUndoView::setStack(QUndoStack *stack) +{ + Q_D(QUndoView); +#ifndef QT_NO_UNDOGROUP + setGroup(0); +#endif + d->model->setStack(stack); +} + +#ifndef QT_NO_UNDOGROUP + +/*! + Sets the group displayed by this view to \a group. If \a group is 0, the view will + be empty. + + The view will update itself autmiatically whenever the active stack of the group changes. + + \sa group() setStack() +*/ + +void QUndoView::setGroup(QUndoGroup *group) +{ + Q_D(QUndoView); + + if (d->group == group) + return; + + if (d->group != 0) { + disconnect(d->group, SIGNAL(activeStackChanged(QUndoStack*)), + d->model, SLOT(setStack(QUndoStack*))); + } + + d->group = group; + + if (d->group != 0) { + connect(d->group, SIGNAL(activeStackChanged(QUndoStack*)), + d->model, SLOT(setStack(QUndoStack*))); + d->model->setStack(d->group->activeStack()); + } else { + d->model->setStack(0); + } +} + +/*! + Returns the group displayed by this view. + + If the view is not looking at group, this function returns 0. + + \sa setGroup() setStack() +*/ + +QUndoGroup *QUndoView::group() const +{ + Q_D(const QUndoView); + return d->group; +} + +#endif // QT_NO_UNDOGROUP + +/*! + \property QUndoView::emptyLabel + \brief the label used for the empty state. + + The empty label is the topmost element in the list of commands, which represents + the state of the document before any commands were pushed on the stack. The default + is the string "<empty>". +*/ + +void QUndoView::setEmptyLabel(const QString &label) +{ + Q_D(QUndoView); + d->model->setEmptyLabel(label); +} + +QString QUndoView::emptyLabel() const +{ + Q_D(const QUndoView); + return d->model->emptyLabel(); +} + +/*! + \property QUndoView::cleanIcon + \brief the icon used to represent the clean state. + + A stack may have a clean state set with QUndoStack::setClean(). This is usually + the state of the document at the point it was saved. QUndoView can display an + icon in the list of commands to show the clean state. If this property is + a null icon, no icon is shown. The default value is the null icon. +*/ + +void QUndoView::setCleanIcon(const QIcon &icon) +{ + Q_D(const QUndoView); + d->model->setCleanIcon(icon); + +} + +QIcon QUndoView::cleanIcon() const +{ + Q_D(const QUndoView); + return d->model->cleanIcon(); +} + +QT_END_NAMESPACE + +#include "qundoview.moc" + +#endif // QT_NO_UNDOVIEW diff --git a/src/widgets/util/qundoview.h b/src/widgets/util/qundoview.h new file mode 100644 index 0000000000..de32a39c54 --- /dev/null +++ b/src/widgets/util/qundoview.h @@ -0,0 +1,102 @@ +/**************************************************************************** +** +** Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies). +** All rights reserved. +** Contact: Nokia Corporation (qt-info@nokia.com) +** +** This file is part of the QtGui module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** No Commercial Usage +** This file contains pre-release code and may not be distributed. +** You may use this file in accordance with the terms and conditions +** contained in the Technology Preview License Agreement accompanying +** this package. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain additional +** rights. These rights are described in the Nokia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** If you have questions regarding the use of this file, please contact +** Nokia at qt-info@nokia.com. +** +** +** +** +** +** +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QUNDOVIEW_H +#define QUNDOVIEW_H + +#include <QtGui/qlistview.h> +#include <QtCore/qstring.h> + +#ifndef QT_NO_UNDOVIEW + +QT_BEGIN_HEADER + +QT_BEGIN_NAMESPACE + +class QUndoViewPrivate; +class QUndoStack; +class QUndoGroup; +class QIcon; + +QT_MODULE(Gui) + +class Q_GUI_EXPORT QUndoView : public QListView +{ + Q_OBJECT + Q_DECLARE_PRIVATE(QUndoView) + Q_PROPERTY(QString emptyLabel READ emptyLabel WRITE setEmptyLabel) + Q_PROPERTY(QIcon cleanIcon READ cleanIcon WRITE setCleanIcon) + +public: + explicit QUndoView(QWidget *parent = 0); + explicit QUndoView(QUndoStack *stack, QWidget *parent = 0); +#ifndef QT_NO_UNDOGROUP + explicit QUndoView(QUndoGroup *group, QWidget *parent = 0); +#endif + ~QUndoView(); + + QUndoStack *stack() const; +#ifndef QT_NO_UNDOGROUP + QUndoGroup *group() const; +#endif + + void setEmptyLabel(const QString &label); + QString emptyLabel() const; + + void setCleanIcon(const QIcon &icon); + QIcon cleanIcon() const; + +public Q_SLOTS: + void setStack(QUndoStack *stack); +#ifndef QT_NO_UNDOGROUP + void setGroup(QUndoGroup *group); +#endif + +private: + Q_DISABLE_COPY(QUndoView) +}; + +QT_END_NAMESPACE + +QT_END_HEADER + +#endif // QT_NO_UNDOVIEW +#endif // QUNDOVIEW_H diff --git a/src/widgets/util/util.pri b/src/widgets/util/util.pri new file mode 100644 index 0000000000..854964b784 --- /dev/null +++ b/src/widgets/util/util.pri @@ -0,0 +1,69 @@ +# Qt util module + +HEADERS += \ + util/qsystemtrayicon.h \ + util/qcompleter.h \ + util/qcompleter_p.h \ + util/qsystemtrayicon_p.h \ + util/qscroller.h \ + util/qscroller_p.h \ + util/qscrollerproperties.h \ + util/qscrollerproperties_p.h \ + util/qflickgesture_p.h \ + util/qundogroup.h \ + util/qundostack.h \ + util/qundostack_p.h \ + util/qundoview.h + +SOURCES += \ + util/qsystemtrayicon.cpp \ + util/qcompleter.cpp \ + util/qscroller.cpp \ + util/qscrollerproperties.cpp \ + util/qflickgesture.cpp \ + util/qundogroup.cpp \ + util/qundostack.cpp \ + util/qundoview.cpp + + +wince* { + SOURCES += \ + util/qsystemtrayicon_wince.cpp +} else:win32 { + SOURCES += \ + util/qsystemtrayicon_win.cpp +} + +unix:x11 { + SOURCES += \ + util/qsystemtrayicon_x11.cpp +} + +qpa { + SOURCES += \ + util/qsystemtrayicon_qpa.cpp +} + +!qpa:!x11:mac { + OBJECTIVE_SOURCES += util/qsystemtrayicon_mac.mm +} + +symbian { + LIBS += -letext -lplatformenv + contains(S60_VERSION, 3.1)|contains(S60_VERSION, 3.2)|contains(S60_VERSION, 5.0) { + LIBS += -lsendas2 -lapmime + contains(QT_CONFIG, s60) { + contains(CONFIG, is_using_gnupoc) { + LIBS += -lcommonui + } else { + LIBS += -lCommonUI + } + } + } else { + DEFINES += USE_SCHEMEHANDLER + } +} + +macx { + OBJECTIVE_SOURCES += util/qscroller_mac.mm +} |