diff --git a/src/formeditor/connectiondialog.cpp b/src/formeditor/connectiondialog.cpp index 5f3595d17..962bbecc3 100644 --- a/src/formeditor/connectiondialog.cpp +++ b/src/formeditor/connectiondialog.cpp @@ -1,444 +1,444 @@ /* This file is part of the KDE project Copyright (C) 2004 Cedric Pasteur Copyright (C) 2004-2009 Jarosław Staniek This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "connectiondialog.h" #include #include "kexitableview.h" #include "events.h" #include "form.h" #include "objecttree.h" #include #include #include #include -#include +#include #include using namespace KFormDesigner; class ConnectionDialog::Private { public: explicit Private(Form *f); ~Private(); Form *form; ConnectionBuffer *buffer; KexiTableView *table; KDbTableViewData *data; KDbTableViewData *widgetsColumnData, *slotsColumnData, *signalsColumnData; QLabel *pixmapLabel, *textLabel; QPushButton *addButton, *removeButton; }; ConnectionDialog::Private::Private(Form *f) :form(f), buffer(0) { } ConnectionDialog::Private::~Private() { } ///////////////////////////////////////////////////////////////////////////////// ///////////// The dialog to edit or add/remove connections ////////////////////// ///////////////////////////////////////////////////////////////////////////////// ConnectionDialog::ConnectionDialog(Form *form, QWidget *parent) : KDialog(parent) , d(new Private(form)) { setObjectName("connections_dialog"); setModal(true); setWindowTitle(xi18nc("@title:window", "Edit Form Connections")); setButtons(KDialog::Ok | KDialog::Cancel | KDialog::Details); setDefaultButton(KDialog::Ok); QFrame *frame = new QFrame(this); setMainWidget(frame); QHBoxLayout *layout = new QHBoxLayout(frame); // Setup the details widget ///////// QWidget *details = new QWidget(frame); layout->addWidget(details); QHBoxLayout *detailsLyr = new QHBoxLayout(details); setDetailsWidget(details); setDetailsWidgetVisible(true); d->pixmapLabel = new QLabel(details); detailsLyr->addWidget(d->pixmapLabel); d->pixmapLabel->setFixedWidth(int(IconSize(KIconLoader::Desktop) * 1.5)); d->pixmapLabel->setAlignment(Qt::AlignHCenter | Qt::AlignTop); d->textLabel = new QLabel(details); detailsLyr->addWidget(d->textLabel); d->textLabel->setAlignment(Qt::AlignLeft | Qt::AlignTop); //setStatusOk(); // And the KexiTableView //////// d->data = new KDbTableViewData(); d->table = new KexiTableView(0, frame, "connections_tableview"); d->table->setSpreadSheetMode(true); d->table->setInsertingEnabled(true); initTable(); d->table->setData(d->data, false); d->table->adjustColumnWidthToContents(0); layout->addWidget(d->table); connect(d->table, SIGNAL(cellSelected(int,int)), this, SLOT(slotCellSelected(int,int))); connect(d->table->data(), SIGNAL(recordInserted(KDbRecordData*,bool)), this, SLOT(slotRecordInserted(KDbRecordData*,bool))); //// Setup the icon toolbar ///////////////// QVBoxLayout *vlayout = new QVBoxLayout(layout); d->addButton = new QPushButton(koIcon("document-new"), xi18n("&New Connection"), frame); vlayout->addWidget(d->addButton); connect(d->addButton, SIGNAL(clicked()), this, SLOT(newItem())); d->removeButton = new QPushButton(koIcon("edit-delete"), xi18n("&Remove Connection"), frame); vlayout->addWidget(d->removeButton); connect(d->removeButton, SIGNAL(clicked()), this, SLOT(removeItem())); vlayout->addStretch(); setInitialSize(QSize(600, 300)); //setWFlags(WDestructiveClose); this->newItem(); } ConnectionDialog::~ConnectionDialog() { delete d; } void ConnectionDialog::initTable() { KDbTableViewColumn *col0 = new KDbTableViewColumn(xi18n("OK?"), KDbField::Text); col0->field()->setSubType("QIcon"); col0->setReadOnly(true); col0->field()->setDescription(xi18n("Connection correctness")); d->data->addColumn(col0); KDbTableViewColumn *col1 = new KDbTableViewColumn(xi18n("Sender"), KDbField::Enum); d->widgetsColumnData = new KDbTableViewData(KDbField::Text, KDbField::Text); col1->setRelatedData(d->widgetsColumnData); d->data->addColumn(col1); KDbTableViewColumn *col2 = new KDbTableViewColumn(xi18n("Signal"), KDbField::Enum); d->signalsColumnData = new KDbTableViewData(KDbField::Text, KDbField::Text); col2->setRelatedData(d->signalsColumnData); d->data->addColumn(col2); KDbTableViewColumn *col3 = new KDbTableViewColumn(xi18n("Receiver"), KDbField::Enum); col3->setRelatedData(d->widgetsColumnData); d->data->addColumn(col3); KDbTableViewColumn *col4 = new KDbTableViewColumn(xi18n("Slot"), KDbField::Enum); d->slotsColumnData = new KDbTableViewData(KDbField::Text, KDbField::Text); col4->setRelatedData(d->slotsColumnData); d->data->addColumn(col4); QList c; c << 2 << 4; d->table->maximizeColumnsWidth(c); d->table->setColumnResizeEnabled(4, true); connect(d->data, SIGNAL(aboutToChangeCell(KDbRecordData*,int,QVariant*,KDbResultInfo*)), this, SLOT(slotCellChanged(KDbRecordData*,int,QVariant*,KDbResultInfo*))); connect(d->data, SIGNAL(recordUpdated(KDbRecordData*)), this, SLOT(checkConnection(KDbRecordData*))); connect(d->table, SIGNAL(itemSelected(KDbRecordData*)), this, SLOT(checkConnection(KDbRecordData*))); } void ConnectionDialog::exec() { updateTableData(); KDialog::exec(); } void ConnectionDialog::slotCellSelected(int row, int col) { d->removeButton->setEnabled(row < d->table->rows()); KDbRecordData *data = d->table->itemAt(row); if (!data) return; if (col == 2) // signal col updateSignalList(data); else if (col == 4) // slot col updateSlotList(data); } void ConnectionDialog::slotRecordInserted(KDbRecordData* item, bool) { d->buffer->append(new Connection()); checkConnection(item); } void ConnectionDialog::slotOk() { // First we update our buffer contents for (int i = 0; i < d->table->rows(); i++) { KDbRecordData *data = d->table->itemAt(i); Connection *c = d->buffer->at(i); c->setSender((*data)[1].toString()); c->setSignal((*data)[2].toString()); c->setReceiver((*data)[3].toString()); c->setSlot((*data)[4].toString()); } // then me make it replace form's current one d->form->setConnectionBuffer(d->buffer); QDialog::accept(); } void ConnectionDialog::updateTableData() { // First we update the columns data foreach (ObjectTreeItem *item, *d->form->objectTree()->hash()) { KDbRecordData *data = d->widgetsColumnData->createItem(); (*data)[0] = item->name(); (*data)[1] = (*data)[0]; d->widgetsColumnData->append(data); } // Then we fill the columns with the form connections foreach (Connection *c, *d->form->connectionBuffer()) { KDbRecordData *newData = d->table->data()->createItem(); (*newData )[1] = c->sender(); (*newData )[2] = c->signal(); (*newData )[3] = c->receiver(); (*newData )[4] = c->slot(); d->table->insertItem(newData , d->table->rows()); } d->buffer = new ConnectionBuffer(*(d->form->connectionBuffer())); } void ConnectionDialog::setStatusOk(KDbRecordData *data) { d->pixmapLabel->setPixmap(koDesktopIcon("dialog-ok")); d->textLabel->setText(QString("

%1

").arg(xi18n("The connection is OK."))); if (!data) data = d->table->selectedItem(); if (d->table->currentRow() >= d->table->rows()) data = 0; if (data) (*data)[0] = "dialog-ok"; else { d->pixmapLabel->setPixmap(QPixmap()); d->textLabel->setText(QString()); } } void ConnectionDialog::setStatusError(const QString &msg, KDbRecordData *data) { d->pixmapLabel->setPixmap(koDesktopIcon("dialog-cancel")); d->textLabel->setText(QString("

%1

").arg(xi18n("The connection is invalid.")) + msg); if (!data) data = d->table->selectedItem(); if (d->table->currentRow() >= d->table->rows()) data = 0; if (data) (*data)[0] = "dialog-cancel"; else { d->pixmapLabel->setPixmap(QPixmap()); d->textLabel->setText(QString()); } } void ConnectionDialog::slotCellChanged(KDbRecordData *data, int col, QVariant*, KDbResultInfo*) { switch (col) { // sender changed, we clear siganl and slot case 1: (*data)[2] = QString(""); // signal or receiver changed, we clear the slot cell case 2: case 3: { (*data)[4] = QString(""); break; } default: break; } } void ConnectionDialog::updateSlotList(KDbRecordData *data) { d->slotsColumnData->deleteAllRecords(); QString widget = (*data)[1].toString(); QString signal = (*data)[2].toString(); if ((widget.isEmpty()) || signal.isEmpty()) return; ObjectTreeItem *tree = d->form->objectTree()->lookup(widget); if (!tree || !tree->widget()) return; QString signalArg(signal); - signalArg.remove(QRegExp(".*[(]|[)]")); + signalArg.remove(QRegularExpression(".*[(]|[)]")); const QList list( KexiUtils::methodsForMetaObjectWithParents(tree->widget()->metaObject(), QMetaMethod::Slot, QMetaMethod::Public)); foreach(const QMetaMethod &method, list) { // we add the slot only if it is compatible with the signal QString slotArg(method.signature()); - slotArg.remove(QRegExp(".*[(]|[)]")); + slotArg.remove(QRegularExpression(".*[(]|[)]")); if (!signalArg.startsWith(slotArg, Qt::CaseSensitive) && (!signal.isEmpty())) // args not compatible continue; KDbRecordData *newData = d->slotsColumnData->createItem(); (*newData)[0] = QString::fromLatin1(method.signature()); (*newData)[1] = (*newData)[0]; d->slotsColumnData->append(newData); } } void ConnectionDialog::updateSignalList(KDbRecordData *data) { ObjectTreeItem *tree = d->form->objectTree()->lookup((*data)[1].toString()); if (!tree || !tree->widget()) return; d->signalsColumnData->deleteAllRecords(); const QList list( KexiUtils::methodsForMetaObjectWithParents(tree->widget()->metaObject(), QMetaMethod::Signal, QMetaMethod::Public)); foreach(const QMetaMethod &method, list) { KDbRecordData *newData = d->signalsColumnData->createItem(); (*newData )[0] = QString::fromLatin1(method.signature()); (*newData )[1] = (*newData )[0]; d->signalsColumnData->append(newData ); } } void ConnectionDialog::checkConnection(KDbRecordData *data) { // First we check if one column is empty for (int i = 1; i < 5; i++) { if (!data || (*data)[i].toString().isEmpty()) { setStatusError(xi18n("You have not selected item: %1.", d->data->column(i)->captionAliasOrName()), data); return; } } // Then we check if signal/slot args are compatible QString signal = (*data)[2].toString(); - signal.remove(QRegExp(".*[(]|[)]")); // just keep the args list + signal.remove(QRegularExpression(".*[(]|[)]")); // just keep the args list QString slot = (*data)[4].toString(); - slot.remove(QRegExp(".*[(]|[)]")); + slot.remove(QRegularExpression(".*[(]|[)]")); if (!signal.startsWith(slot, Qt::CaseSensitive)) { setStatusError(xi18n("The signal/slot arguments are not compatible."), data); return; } setStatusOk(data); } void ConnectionDialog::newItem() { d->table->acceptRecordEditing(); d->table->setCursorPosition(d->table->rows(), 1); } void ConnectionDialog::newItemByDragnDrop() { d->form->enterConnectingState(); connect(d->form, SIGNAL(connectionAborted(KFormDesigner::Form*)), this, SLOT(slotConnectionAborted(KFormDesigner::Form*))); connect(d->form, SIGNAL(connectionCreated(KFormDesigner::Form*,Connection&)), this, SLOT(slotConnectionCreated(KFormDesigner::Form*,Connection&))); hide(); } void ConnectionDialog::slotConnectionCreated(KFormDesigner::Form *form, Connection &connection) { show(); if (form != d->form) return; Connection *c = new Connection(connection); KDbRecordData *newData = d->table->data()->createItem(); (*newData)[1] = c->sender(); (*newData)[2] = c->signal(); (*newData)[3] = c->receiver(); (*newData)[4] = c->slot(); d->table->insertItem(newData, d->table->rows()); d->buffer->append(c); } void ConnectionDialog::slotConnectionAborted(KFormDesigner::Form *form) { show(); if (form != d->form) return; newItem(); } void ConnectionDialog::removeItem() { if (d->table->currentRow() == -1 || d->table->currentRow() >= d->table->rows()) return; if (KMessageBox::Yes != KMessageBox::questionYesNo(parentWidget(), xi18n("Do you want to delete this connection?"), QString(), KGuiItem(xi18nc("@action:button", "&Delete Connection")), KStandardGuiItem::no(), "AskBeforeDeleteConnection"/*config entry*/)) { return; } d->buffer->removeAt(d->table->currentRow()); d->table->deleteItem(d->table->selectedItem()); } //! @todo KEXI3 noi18n # added to disable message extraction in Messages.sh diff --git a/src/kexiutils/completer/KexiCompleter.cpp b/src/kexiutils/completer/KexiCompleter.cpp index 67ffb0dcf..726424e7a 100644 --- a/src/kexiutils/completer/KexiCompleter.cpp +++ b/src/kexiutils/completer/KexiCompleter.cpp @@ -1,1915 +1,1915 @@ /**************************************************************************** ** ** 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$ ** GNU Lesser General Public License Usage ** 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. ** ** GNU General Public License Usage ** Alternatively, this file may be used under the terms of the GNU General ** Public License version 3.0 as published by the Free Software Foundation ** and appearing in the file LICENSE.GPL included in the packaging of this ** file. Please review the following information to ensure the GNU General ** Public License version 3.0 requirements will be met: ** http://www.gnu.org/copyleft/gpl.html. ** ** Other Usage ** Alternatively, this file may be used in accordance with the terms and ** conditions contained in a signed written agreement between you and Nokia. ** ** ** ** ** ** $QT_END_LICENSE$ ** ****************************************************************************/ /*! \class KexiCompleter \brief The KexiCompleter class provides completions based on an item model. \since 4.2 You can use KexiCompleter to provide auto completions in any Qt widget, such as QLineEdit and QComboBox. When the user starts typing a word, KexiCompleter 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 KexiCompleter's constructor.) \tableofcontents \section1 Basic Usage A KexiCompleter 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 KexiCompleter should operate, call setModel(). By default, KexiCompleter 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 KexiCompleter::CaseSensitivelySortedModel or KexiCompleter::CaseInsensitivelySortedModel as the argument. On large models, this can lead to significant performance improvements, because KexiCompleter 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 KexiCompleter 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, KexiCompleter might suggest \c Windows to complete the current path element. Similarly, if the current text is \c C:\Windows\Sy, KexiCompleter might suggest \c System. For this kind of completion to work, KexiCompleter 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, KexiCompleter 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 "KexiCompleter_p.h" #ifndef QT_NO_COMPLETER #include #include #include #include #include #include #include #include #include #include #include class KexiEmptyItemModel : public QAbstractItemModel { public: explicit KexiEmptyItemModel(QObject *parent = 0) : QAbstractItemModel(parent) {} QModelIndex index(int, int, const QModelIndex &) const { return QModelIndex(); } QModelIndex parent(const QModelIndex &) const { return QModelIndex(); } int rowCount(const QModelIndex &) const { return 0; } int columnCount(const QModelIndex &) const { return 0; } bool hasChildren(const QModelIndex &) const { return false; } QVariant data(const QModelIndex &, int) const { return QVariant(); } }; Q_GLOBAL_STATIC(KexiEmptyItemModel, kexiEmptyModel) QAbstractItemModel *KexiAbstractItemModelPrivate::staticEmptyModel() { return kexiEmptyModel(); } namespace { struct DefaultRoleNames : public QHash { DefaultRoleNames() { (*this)[Qt::DisplayRole] = "display"; (*this)[Qt::DecorationRole] = "decoration"; (*this)[Qt::EditRole] = "edit"; (*this)[Qt::ToolTipRole] = "toolTip"; (*this)[Qt::StatusTipRole] = "statusTip"; (*this)[Qt::WhatsThisRole] = "whatsThis"; } }; } Q_GLOBAL_STATIC(DefaultRoleNames, qDefaultRoleNames) const QHash &KexiAbstractItemModelPrivate::defaultRoleNames() { return *qDefaultRoleNames(); } KexiCompletionModel::KexiCompletionModel(KexiCompleterPrivate *c, QObject *parent) : QAbstractProxyModel(parent), c(c), showAll(false), d(new KexiCompletionModelPrivate(this)) { QAbstractProxyModel::setSourceModel(KexiAbstractItemModelPrivate::staticEmptyModel()); createEngine(); } KexiCompletionModel::~KexiCompletionModel() { delete d; } int KexiCompletionModel::columnCount(const QModelIndex &) const { return sourceModel()->columnCount(); } void KexiCompletionModel::setSourceModel(QAbstractItemModel *source) { bool hadModel = (sourceModel() != 0); if (hadModel) QObject::disconnect(sourceModel(), 0, this, 0); QAbstractProxyModel::setSourceModel(source ? source : KexiAbstractItemModelPrivate::staticEmptyModel()); 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 KexiCompletionModel::createEngine() { bool sortedEngine = false; switch (c->sorting) { case KexiCompleter::UnsortedModel: sortedEngine = false; break; case KexiCompleter::CaseSensitivelySortedModel: sortedEngine = c->cs == Qt::CaseSensitive; break; case KexiCompleter::CaseInsensitivelySortedModel: sortedEngine = c->cs == Qt::CaseInsensitive; break; } if (sortedEngine) engine.reset(new QSortedModelEngine(c)); else engine.reset(new QUnsortedModelEngine(c)); } QModelIndex KexiCompletionModel::mapToSource(const QModelIndex& index) const { 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()); KexiIndexMapper& 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 sourceModel()->index(row, index.column(), parent); } QModelIndex KexiCompletionModel::mapFromSource(const QModelIndex& idx) const { if (!idx.isValid()) return QModelIndex(); int row = -1; if (!showAll) { if (!engine->matchCount()) return QModelIndex(); KexiIndexMapper& 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) { KexiIndexMapper& 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 KexiCompletionModel::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 KexiCompletionModel::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 KexiCompletionModel::index(int row, int column, const QModelIndex& parent) const { 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 >= sourceModel()->rowCount(engine->curParent)) return QModelIndex(); } return createIndex(row, column); } int KexiCompletionModel::completionCount() const { if (!engine->matchCount()) return 0; engine->filterOnDemand(INT_MAX); return engine->matchCount(); } int KexiCompletionModel::rowCount(const QModelIndex &parent) const { 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 sourceModel()->rowCount(engine->curParent); } return completionCount(); } void KexiCompletionModel::setFiltered(bool filtered) { if (showAll == !filtered) return; showAll = !filtered; resetModel(); } bool KexiCompletionModel::hasChildren(const QModelIndex &parent) const { if (parent.isValid()) return false; if (showAll) return sourceModel()->hasChildren(mapToSource(parent)); if (!engine->matchCount()) return false; return true; } QVariant KexiCompletionModel::data(const QModelIndex& index, int role) const { return sourceModel()->data(mapToSource(index), role); } void KexiCompletionModel::modelDestroyed() { QAbstractProxyModel::setSourceModel(0); // switch to static empty model invalidate(); } void KexiCompletionModel::rowsInserted() { invalidate(); emit rowsAdded(); } void KexiCompletionModel::invalidate() { engine->cache.clear(); filter(engine->curParts); } void KexiCompletionModel::filter(const QStringList& parts) { engine->filter(parts); resetModel(); if (sourceModel()->canFetchMore(engine->curParent)) sourceModel()->fetchMore(engine->curParent); } void KexiCompletionModel::resetModel() { if (rowCount() == 0) { beginResetModel(); endResetModel(); 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 KexiCompletionEngine::filter(const QStringList& parts) { const QAbstractItemModel *model = c->proxy->sourceModel(); curParts = parts; if (curParts.isEmpty()) curParts.append(QString()); curRow = -1; curParent = QModelIndex(); curMatch = KexiMatchData(); 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 = KexiMatchData(KexiIndexMapper(0, model->rowCount(curParent) - 1), -1, false); else curMatch = filter(curParts.last(), curParent, 1); // build at least one curRow = curMatch.isValid() ? 0 : -1; } inline bool matchPrefix(const QString& s1, const QString& s2, Qt::CaseSensitivity cs) { return s1.startsWith(s2, cs); } inline bool matchSubstring(const QString& s1, const QString& s2, Qt::CaseSensitivity cs) { return s1.contains(s2, cs); } typedef bool (*MatchFunction)(const QString&, const QString&, Qt::CaseSensitivity); KexiMatchData KexiCompletionEngine::filterHistory() { QAbstractItemModel *source = c->proxy->sourceModel(); if (curParts.count() <= 1 || c->proxy->showAll || !source) return KexiMatchData(); #ifdef QT_NO_DIRMODEL bool isDirModel = false; #else bool isDirModel = qobject_cast(source) != 0; #endif #ifdef QT_NO_FILESYSTEMMODEL bool isFsModel = false; #else bool isFsModel = qobject_cast(source) != 0; #endif QVector v; KexiIndexMapper im(v); KexiMatchData m(im, -1, true); MatchFunction matchFunction = c->substringCompletion ? &matchSubstring : &matchPrefix; for (int i = 0; i < source->rowCount(); i++) { QString str = source->index(i, c->column).data().toString(); if (matchFunction(str, 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 KexiCompletionEngine::matchHint(QString part, const QModelIndex& parent, KexiMatchData *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 KexiCompletionEngine::lookupCache(QString part, const QModelIndex& parent, KexiMatchData *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 KexiCompletionEngine::saveInCache(QString part, const QModelIndex& parent, const KexiMatchData& m) { KexiMatchData old = cache[parent].take(part); cost = cost + m.indices.cost() - old.indices.cost(); if (cost * sizeof(int) > 1024 * 1024) { QMap::iterator it1 = cache.begin(); while (it1 != cache.end()) { CacheItem& ci = it1.value(); int sz = ci.count()/2; QMap::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; } /////////////////////////////////////////////////////////////////////////////////// KexiIndexMapper 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 KexiMatchData& 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 KexiMatchData& 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 KexiIndexMapper(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; } KexiMatchData QSortedModelEngine::filter(const QString& part, const QModelIndex& parent, int) { const QAbstractItemModel *model = c->proxy->sourceModel(); KexiMatchData hint; if (lookupCache(part, parent, &hint)) return hint; KexiIndexMapper indices; Qt::SortOrder order = sortOrder(parent); if (matchHint(part, parent, &hint)) { if (!hint.isValid()) return KexiMatchData(); 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, KexiMatchData()); return KexiMatchData(); } 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, KexiMatchData()); return KexiMatchData(); } 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; } } KexiMatchData m(order == Qt::AscendingOrder ? KexiIndexMapper(from, high - 1) : KexiIndexMapper(low+1, to), emi, false); saveInCache(part, parent, m); return m; } //////////////////////////////////////////////////////////////////////////////////////// int QUnsortedModelEngine::buildIndices(const QString& str, const QModelIndex& parent, int n, const KexiIndexMapper& indices, KexiMatchData* m) { Q_ASSERT(m->partial); Q_ASSERT(n != -1 || m->exactMatchIndex == -1); const QAbstractItemModel *model = c->proxy->sourceModel(); int i, count = 0; MatchFunction matchFunction = c->substringCompletion ? &matchSubstring : &matchPrefix; 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 (!matchFunction(data, 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; KexiIndexMapper im(curMatch.indices.last() + 1, lastRow); int lastIndex = buildIndices(curParts.last(), curParent, n, im, &curMatch); curMatch.partial = (lastRow != lastIndex); saveInCache(curParts.last(), curParent, curMatch); } KexiMatchData QUnsortedModelEngine::filter(const QString& part, const QModelIndex& parent, int n) { KexiMatchData hint; QVector v; KexiIndexMapper im(v); KexiMatchData 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 KexiMatchData(); } if (!foundInCache && !hint.isValid()) { const int lastRow = model->rowCount(parent) - 1; KexiIndexMapper 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; KexiIndexMapper 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; } /////////////////////////////////////////////////////////////////////////////// KexiCompleterPrivate::KexiCompleterPrivate(KexiCompleter *qq) : widget(0), proxy(0), popup(0), cs(Qt::CaseSensitive), substringCompletion(false), role(Qt::EditRole), column(0), maxVisibleItems(7), sorting(KexiCompleter::UnsortedModel), wrap(true), eatFocusOut(true), hiddenBecauseNoMatch(false), q(qq) { } void KexiCompleterPrivate::init(QAbstractItemModel *m) { proxy = new KexiCompletionModel(this, q); QObject::connect(proxy, SIGNAL(rowsAdded()), q, SLOT(_q_autoResizePopup())); q->setModel(m); #ifdef QT_NO_LISTVIEW q->setCompletionMode(KexiCompleter::InlineCompletion); #else q->setCompletionMode(KexiCompleter::PopupCompletion); #endif // QT_NO_LISTVIEW } void KexiCompleterPrivate::setCurrentIndex(QModelIndex index, bool select) { 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 KexiCompleterPrivate::_q_completionSelected(const QItemSelection& selection) { QModelIndex index; if (!selection.indexes().isEmpty()) index = selection.indexes().first(); _q_complete(index, true); } void KexiCompleterPrivate::_q_complete(QModelIndex index, bool highlighted) { 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 == KexiCompleter::InlineCompletion) { if (qobject_cast(proxy->sourceModel()) && QFileInfo(completion).isDir()) completion += QDir::separator(); } #endif #ifndef QT_NO_FILESYSTEMMODEL // add a trailing separator in inline if (mode == KexiCompleter::InlineCompletion) { if (qobject_cast(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 KexiCompleterPrivate::_q_autoResizePopup() { if (!popup || !popup->isVisible()) return; showPopup(popupRect); } static void adjustPopupGeometry(QWidget *popupWidget, QWidget *widget, int widthHint, int heightHint, const QRect ¤tRect) { const QRect screen = QApplication::desktop()->availableGeometry(widget); const Qt::LayoutDirection dir = widget->layoutDirection(); QPoint pos; int rh, w; int h = heightHint; if (currentRect.isValid()) { rh = currentRect.height(); w = currentRect.width(); pos = widget->mapToGlobal(dir == Qt::RightToLeft ? currentRect.bottomRight() : currentRect.bottomLeft()); } else { rh = widget->height(); pos = widget->mapToGlobal(QPoint(0, widget->height() - 2)); w = widget->width(); } if (widthHint > w) { w = widthHint; } 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, popupWidget->minimumHeight()); if (h > bottom) { h = qMin(qMax(top, bottom), h); if (top > bottom) pos.setY(pos.y() - h - rh + 2); } popupWidget->setGeometry(pos.x(), pos.y(), w, h); } void KexiCompleterPrivate::showPopup(const QRect& rect) { int widthHint = popup->sizeHintForColumn(0); QScrollBar *vsb = popup->verticalScrollBar(); if (vsb) { widthHint += vsb->sizeHint().width() + 3 + 3; } int heightHint = (popup->sizeHintForRow(0) * qMin(maxVisibleItems, popup->model()->rowCount()) + 3) + 3; QScrollBar *hsb = popup->horizontalScrollBar(); if (hsb && hsb->isVisible()) { heightHint += hsb->sizeHint().height(); } adjustPopupGeometry(popup, widget, widthHint, heightHint, rect); if (!popup->isVisible()) popup->show(); } void KexiCompleterPrivate::_q_fileSystemModelDirectoryLoaded(const QString &path) { // 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. */ KexiCompleter::KexiCompleter(QObject *parent) : QObject(parent), d(new KexiCompleterPrivate(this)) { d->init(); } /*! Constructs a completer object with the given \a parent that provides completions from the specified \a model. */ KexiCompleter::KexiCompleter(QAbstractItemModel *model, QObject *parent) : QObject(parent), d(new KexiCompleterPrivate(this)) { d->init(model); } #ifndef QT_NO_STRINGLISTMODEL /*! Constructs a KexiCompleter object with the given \a parent that uses the specified \a list as a source of possible completions. */ KexiCompleter::KexiCompleter(const QStringList& list, QObject *parent) : QObject(parent), d(new KexiCompleterPrivate(this)) { d->init(new QStringListModel(list, this)); } #endif // QT_NO_STRINGLISTMODEL /*! Destroys the completer object. */ KexiCompleter::~KexiCompleter() { delete d; } /*! Sets the widget for which completion are provided for to \a widget. This function is automatically called when a KexiCompleter 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 KexiCompleter::setWidget(QWidget *widget) { if (widget && d->widget == widget) return; 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 *KexiCompleter::widget() const { 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 KexiCompleter as its parent, it is deleted. For convenience, if \a model is a QFileSystemModel, KexiCompleter switches its caseSensitivity to Qt::CaseInsensitive on Windows and Qt::CaseSensitive on other platforms. \sa completionModel(), modelSorting, {Handling Tree Models} */ void KexiCompleter::setModel(QAbstractItemModel *model) { 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(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(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 *KexiCompleter::model() const { return d->proxy->sourceModel(); } /*! \enum KexiCompleter::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 KexiCompleter::completionMode \brief how the completions are provided to the user The default value is KexiCompleter::PopupCompletion. */ void KexiCompleter::setCompletionMode(KexiCompleter::CompletionMode mode) { d->mode = mode; d->proxy->setFiltered(mode != KexiCompleter::UnfilteredPopupCompletion); if (mode == KexiCompleter::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); } } KexiCompleter::CompletionMode KexiCompleter::completionMode() const { return d->mode; } /*! Sets the popup used to display completions to \a popup. KexiCompleter takes ownership of the view. A QListView is automatically created when the completionMode() is set to KexiCompleter::PopupCompletion or KexiCompleter::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 KexiCompleter::setPopup(QAbstractItemView *popup) { 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 KexiCompleterItemDelegate(popup)); #ifndef QT_NO_LISTVIEW if (QListView *listView = qobject_cast(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 *KexiCompleter::popup() const { #ifndef QT_NO_LISTVIEW if (!d->popup && completionMode() != KexiCompleter::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); KexiCompleter *that = const_cast(this); that->setPopup(listView); } #endif // QT_NO_LISTVIEW return d->popup; } /*! \reimp */ bool KexiCompleter::event(QEvent *ev) { return QObject::event(ev); } /*! \reimp */ bool KexiCompleter::eventFilter(QObject *o, QEvent *e) { 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(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 == KexiCompleter::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(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(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(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(o); if (source) { QPoint pos = source->mapToGlobal((static_cast(e))->pos()); QWidget *target = QApplication::widgetAt(pos); if (target && (d->widget->isAncestorOf(target) || target == d->widget)) { d->eatFocusOut = false; static_cast(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 KexiCompleter::PopupCompletion and KexiCompleter::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 KexiCompleter::InlineCompletion mode, the highlighted() signal is fired with the current completion. */ void KexiCompleter::complete(const QRect& rect) { QModelIndex idx = d->proxy->currentIndex(false); d->hiddenBecauseNoMatch = false; if (d->mode == KexiCompleter::InlineCompletion) { if (idx.isValid()) d->_q_complete(idx, true); return; } Q_ASSERT(d->widget != 0); if ((d->mode == KexiCompleter::PopupCompletion && !idx.isValid()) || (d->mode == KexiCompleter::UnfilteredPopupCompletion && d->proxy->rowCount() == 0)) { if (d->popup) d->popup->hide(); // no suggestion, hide d->hiddenBecauseNoMatch = true; return; } popup(); if (d->mode == KexiCompleter::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 KexiCompleter::setCurrentRow(int row) { return d->proxy->setCurrentRow(row); } /*! Returns the current row. \sa setCurrentRow() */ int KexiCompleter::currentRow() const { 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 KexiCompleter::completionCount() const { return d->proxy->completionCount(); } /*! \enum KexiCompleter::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 KexiCompleter::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(), KexiCompleter::ModelSorting */ void KexiCompleter::setModelSorting(KexiCompleter::ModelSorting sorting) { if (d->sorting == sorting) return; d->sorting = sorting; d->proxy->createEngine(); d->proxy->invalidate(); } KexiCompleter::ModelSorting KexiCompleter::modelSorting() const { return d->sorting; } /*! \property KexiCompleter::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 KexiCompleter::setCompletionColumn(int column) { if (d->column == column) return; #ifndef QT_NO_LISTVIEW if (QListView *listView = qobject_cast(d->popup)) listView->setModelColumn(column); #endif d->column = column; d->proxy->invalidate(); } int KexiCompleter::completionColumn() const { return d->column; } /*! \property KexiCompleter::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 KexiCompleter::setCompletionRole(int role) { if (d->role == role) return; d->role = role; d->proxy->invalidate(); } int KexiCompleter::completionRole() const { return d->role; } /*! \property KexiCompleter::wrapAround \brief the completions wrap around when navigating through items \since 4.3 The default is true. */ void KexiCompleter::setWrapAround(bool wrap) { if (d->wrap == wrap) return; d->wrap = wrap; } bool KexiCompleter::wrapAround() const { return d->wrap; } /*! \property KexiCompleter::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 KexiCompleter::maxVisibleItems() const { return d->maxVisibleItems; } void KexiCompleter::setMaxVisibleItems(int maxItems) { if (maxItems < 0) { qWarning("KexiCompleter::setMaxVisibleItems: " "Invalid max visible items (%d) must be >= 0", maxItems); return; } d->maxVisibleItems = maxItems; } /*! \property KexiCompleter::caseSensitivity \brief the case sensitivity of the matching The default is Qt::CaseSensitive. \sa substringCompletion, completionColumn, completionRole, modelSorting */ void KexiCompleter::setCaseSensitivity(Qt::CaseSensitivity cs) { if (d->cs == cs) return; d->cs = cs; d->proxy->createEngine(); d->proxy->invalidate(); } Qt::CaseSensitivity KexiCompleter::caseSensitivity() const { return d->cs; } /*! \property KexiCompleter::substringCompletion \brief the completion uses any substring matching If true the completion uses any substring matching. If false prefix matching is used. The default is false. \sa caseSensitivity */ void KexiCompleter::setSubstringCompletion(bool substringCompletion) { if (d->substringCompletion == substringCompletion) return; d->substringCompletion = substringCompletion; d->proxy->invalidate(); } bool KexiCompleter::substringCompletion() const { return d->substringCompletion; } /*! \property KexiCompleter::completionPrefix \brief the completion prefix used to provide completions. The completionModel() is updated to reflect the list of possible matches for \a prefix. */ void KexiCompleter::setCompletionPrefix(const QString &prefix) { d->prefix = prefix; d->proxy->filter(splitPath(prefix)); } QString KexiCompleter::completionPrefix() const { return d->prefix; } /*! Returns the model index of the current completion in the completionModel(). \sa setCurrentRow(), currentCompletion(), model() */ QModelIndex KexiCompleter::currentIndex() const { 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 KexiCompleter::currentCompletion() const { 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 *KexiCompleter::completionModel() const { 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 KexiCompleter::pathFromIndex(const QModelIndex& index) const { 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(d->proxy->sourceModel()) != 0; #endif #ifndef QT_NO_FILESYSTEMMODEL isFsModel = qobject_cast(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 KexiCompleter::splitPath(const QString& path) const { bool isDirModel = false; bool isFsModel = false; #ifndef QT_NO_DIRMODEL isDirModel = qobject_cast(d->proxy->sourceModel()) != 0; #endif #ifndef QT_NO_FILESYSTEMMODEL #ifdef QT_NO_DIRMODEL #endif isFsModel = qobject_cast(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.remove(0, 2); else doubleSlash.clear(); #endif - QRegExp re(QLatin1Char('[') + QRegExp::escape(sep) + QLatin1Char(']')); + QRegularExpression re(QLatin1Char('[') + QRegularExpression::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 KexiCompleter::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 KexiCompleter::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 KexiCompleter::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 KexiCompleter::InlineCompletion. The item's \a index in the completionModel() is given. */ /*! \fn void KexiCompleter::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 KexiCompleter::InlineCompletion. The item's \a text is given. */ void KexiCompleter::_q_complete(const QModelIndex& index) { d->_q_complete(index); } void KexiCompleter::_q_completionSelected(const QItemSelection& selection) { d->_q_completionSelected(selection); } void KexiCompleter::_q_autoResizePopup() { d->_q_autoResizePopup(); } void KexiCompleter::_q_fileSystemModelDirectoryLoaded(const QString& dir) { d->_q_fileSystemModelDirectoryLoaded(dir); } KexiCompletionModelPrivate::KexiCompletionModelPrivate(KexiCompletionModel *q) : q(q) { } void KexiCompletionModelPrivate::_q_sourceModelDestroyed() { q->setSourceModel(KexiAbstractItemModelPrivate::staticEmptyModel()); } #endif // QT_NO_COMPLETER diff --git a/src/main/startup/KexiWelcomeStatusBar.cpp b/src/main/startup/KexiWelcomeStatusBar.cpp index 114c9eb49..9de211c01 100644 --- a/src/main/startup/KexiWelcomeStatusBar.cpp +++ b/src/main/startup/KexiWelcomeStatusBar.cpp @@ -1,1107 +1,1108 @@ /* This file is part of the KDE project Copyright (C) 2011-2012 Jarosław Staniek This program is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "KexiWelcomeStatusBar.h" #include "KexiWelcomeStatusBar_p.h" #include #include #include #include #include #include "KexiUserFeedbackAgent.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include +#include #include static const int GUI_UPDATE_INTERVAL = 60; // update interval for GUI, in minutes static const int DONATION_INTERVAL = 10; // donation interval, in days static const int UPDATE_FILES_LIST_SIZE_LIMIT = 1024 * 128; static const int UPDATE_FILES_COUNT_LIMIT = 128; //! @return x.y.0 static QString stableVersionStringDot0() { return QString::number(Kexi::stableVersionMajor()) + '.' + QString::number(Kexi::stableVersionMinor()) + ".0"; } static QString basePath() { return QString("kexi/status/") + stableVersionStringDot0(); } static QString findFilename(const QString &guiFileName) { const QString result = QStandardPaths::locate(QStandardPaths::GenericDataLocation, basePath() + '/' + guiFileName); //qDebug() << result; return result; } // --- class KexiWelcomeStatusBarGuiUpdater::Private { public: Private() : configGroup(KConfigGroup(KSharedConfig::openConfig()->group("User Feedback"))) { } KConfigGroup configGroup; QStringList fileNamesToUpdate; QString tempDir; }; KexiWelcomeStatusBarGuiUpdater::KexiWelcomeStatusBarGuiUpdater() : QObject() , d(new Private) { } KexiWelcomeStatusBarGuiUpdater::~KexiWelcomeStatusBarGuiUpdater() { delete d; } QString KexiWelcomeStatusBarGuiUpdater::uiPath(const QString &fname) const { KexiUserFeedbackAgent *f = KexiMainWindowIface::global()->userFeedbackAgent(); return f->serviceUrl() + QString("/ui/%1/").arg(stableVersionStringDot0()) + fname; } void KexiWelcomeStatusBarGuiUpdater::update() { QDateTime lastStatusBarUpdate = d->configGroup.readEntry("LastStatusBarUpdate", QDateTime()); if (lastStatusBarUpdate.isValid()) { int minutes = lastStatusBarUpdate.secsTo(QDateTime::currentDateTime()) / 60; if (minutes < GUI_UPDATE_INTERVAL) { qDebug() << "gui updated" << minutes << "min. ago, next auto-update in" << (GUI_UPDATE_INTERVAL - minutes) << "min."; return; } } d->configGroup.writeEntry("LastStatusBarUpdate", QDateTime::currentDateTime()); KexiUserFeedbackAgent *f = KexiMainWindowIface::global()->userFeedbackAgent(); f->waitForRedirect(this, "slotRedirectLoaded"); } void KexiWelcomeStatusBarGuiUpdater::slotRedirectLoaded() { QByteArray postData = stableVersionStringDot0().toLatin1(); KIO::Job* sendJob = KIO::storedHttpPost(postData, QUrl(uiPath(".list")), KIO::HideProgressInfo); connect(sendJob, SIGNAL(result(KJob*)), this, SLOT(sendRequestListFilesFinished(KJob*))); sendJob->addMetaData("content-type", "Content-Type: application/x-www-form-urlencoded"); } void KexiWelcomeStatusBarGuiUpdater::sendRequestListFilesFinished(KJob* job) { if (job->error()) { qWarning() << "Error while receiving .list file - no files will be updated"; //! @todo error... return; } KIO::StoredTransferJob* sendJob = qobject_cast(job); QString result = sendJob->data(); if (result.length() > UPDATE_FILES_LIST_SIZE_LIMIT) { // anit-DOS protection qWarning() << "Too large .list file (" << result.length() << "); the limit is" << UPDATE_FILES_LIST_SIZE_LIMIT << "- no files will be updated"; return; } qDebug() << result; QStringList data = result.split('\n', QString::SkipEmptyParts); result.clear(); d->fileNamesToUpdate.clear(); if (data.count() > UPDATE_FILES_COUNT_LIMIT) { // anti-DOS protection qWarning() << "Too many files to update (" << data.count() << "); the limit is" << UPDATE_FILES_COUNT_LIMIT << "- no files will be updated"; return; } // OK, try to update (stage 1: check, stage 2: checking) for (int stage = 1; stage <= 2; stage++) { int i = 0; for (QStringList::ConstIterator it(data.constBegin()); it!=data.constEnd(); ++it, i++) { const QByteArray hash((*it).left(32).toLatin1()); const QString remoteFname((*it).mid(32 + 2)); if (stage == 1) { if (hash.length() != 32) { qWarning() << "Invalid hash" << hash << "in line" << i+1 << "- no files will be updated"; return; } if ((*it).mid(32, 2) != " ") { qWarning() << "Two spaces expected but found" << (*it).mid(32, 2) << "in line" << i+1 << "- no files will be updated"; return; } - if (remoteFname.contains(QRegExp("\\s"))) { + if (remoteFname.contains(QRegularExpression("\\s"))) { qWarning() << "Filename expected without whitespace but found" << remoteFname << "in line" << i+1 << "- no files will be updated"; return; } } else if (stage == 2) { checkFile(hash, remoteFname, &d->fileNamesToUpdate); } } } // update files QList sourceFiles; foreach (const QString &fname, d->fileNamesToUpdate) { sourceFiles.append(QUrl(uiPath(fname))); } QTemporaryDir tempDir(QDir::tempPath() + "/kexi-status"); tempDir.setAutoRemove(false); d->tempDir = tempDir.path(); qDebug() << tempDir.path(); KIO::CopyJob *copyJob = KIO::copy(sourceFiles, QUrl::fromLocalFile(tempDir.path()), KIO::HideProgressInfo | KIO::Overwrite); connect(copyJob, SIGNAL(result(KJob*)), this, SLOT(filesCopyFinished(KJob*))); //qDebug() << "copying from" << QUrl(uiPath(fname)) << "to" // << (dir + fname); } void KexiWelcomeStatusBarGuiUpdater::checkFile(const QByteArray &hash, const QString &remoteFname, QStringList *fileNamesToUpdate) { QString localFname = findFilename(remoteFname); if (localFname.isEmpty()) { fileNamesToUpdate->append(remoteFname); qDebug() << "missing filename" << remoteFname << "- download it"; return; } QFile file(localFname); if (!file.open(QIODevice::ReadOnly)) { qWarning() << "could not open file" << localFname << "- update it"; fileNamesToUpdate->append(remoteFname); return; } QCryptographicHash md5(QCryptographicHash::Md5); if (!md5.addData(&file)) { qWarning() << "could not check MD5 for file" << localFname << "- update it"; fileNamesToUpdate->append(remoteFname); return; } if (md5.result().toHex() != hash) { qDebug() << "not matching file" << localFname << "- update it"; fileNamesToUpdate->append(remoteFname); } } void KexiWelcomeStatusBarGuiUpdater::filesCopyFinished(KJob* job) { if (job->error()) { //! @todo error... qDebug() << "ERROR:" << job->errorString(); return; } KIO::CopyJob* copyJob = qobject_cast(job); Q_UNUSED(copyJob) //qDebug() << "DONE" << copyJob->destUrl(); QString dir(QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1Char('/') + basePath() + '/'); bool ok = true; if (!QDir(dir).exists()) { if (!QDir().mkpath(dir)) { ok = false; qWarning() << "Could not create" << dir; } } if (ok) { foreach (const QString &fname, d->fileNamesToUpdate) { const QByteArray oldName(QFile::encodeName(d->tempDir + '/' + fname)), newName(QFile::encodeName(dir + fname)); if (0 != ::rename(oldName.constData(), newName.constData())) { qWarning() << "cannot move" << (d->tempDir + '/' + fname) << "to" << (dir + fname); } } } QDir(d->tempDir).removeRecursively(); } // --- //! @internal class ScrollArea : public QScrollArea { public: explicit ScrollArea(QWidget *parent = 0) : QScrollArea(parent) { setFrameShape(QFrame::NoFrame); setBackgroundRole(QPalette::Base); setWidgetResizable(true); } void setEnabled(bool set) { if (set != isEnabled()) { QScrollArea::setEnabled(set); updateColors(); } } protected: virtual void changeEvent(QEvent* event) { switch (event->type()) { case QEvent::EnabledChange: case QEvent::PaletteChange: updateColors(); break; default:; } QScrollArea::changeEvent(event); } void updateColors() { if (!widget()) return; KColorScheme scheme(palette().currentColorGroup()); QColor linkColor = scheme.foreground(KColorScheme::LinkText).color(); //qDebug() << "_____________" << isEnabled(); foreach(QLabel* lbl, widget()->findChildren()) { QString t = lbl->text(); - QRegExp re(""); - re.setMinimal(true); + QRegularExpression re("", QRegularExpression::InvertedGreedinessOption); int pos = 0; int oldPos = 0; QString newText; + QRegularExpressionMatch match = re.match(t); //qDebug() << "t:" << t; - while ((pos = re.indexIn(t, pos)) != -1) { + while ((pos = match.capturedStart(pos)) != -1) { //qDebug() << "pos:" << pos; //qDebug() << "newText += t.mid(oldPos, pos - oldPos)" // << t.mid(oldPos, pos - oldPos); newText += t.mid(oldPos, pos - oldPos); //qDebug() << "newText1:" << newText; //qDebug() << lbl->objectName() << "~~~~" << t.mid(pos, re.matchedLength()); - QString a = t.mid(pos, re.matchedLength()); + QString a = t.mid(pos, match.capturedLength()); //qDebug() << "a:" << a; int colPos = a.indexOf("color:"); if (colPos == -1) { // add color a.insert(a.length() - 1, " style=\"color:" + linkColor.name() + ";\""); } else { // replace color colPos += qstrlen("color:"); for (;colPos < a.length() && a[colPos] == ' '; colPos++) { } if (colPos < a.length() && a[colPos] == '#') { colPos++; int i = colPos; for (;i < a.length(); i++) { if (a[i] == ';' || a[i] == ' ' || a[i] == '"' || a[i] == '\'') break; } //qDebug() << "******" << a.mid(colPos, i - colPos); a.replace(colPos, i - colPos, linkColor.name().mid(1)); } } //qDebug() << "a2:" << a; newText += a; //qDebug() << "newText2:" << newText; - pos += re.matchedLength(); + pos += match.capturedLength(); oldPos = pos; //qDebug() << "pos2:" << pos; } //qDebug() << "oldPos:" << oldPos; newText += t.mid(oldPos); //qDebug() << "newText3:" << newText; lbl->setText(newText); } #if 0 QString text; text = QString("%3") .arg(link).arg(linkColor.name()).arg(linkText); if (!format.isEmpty()) { text = QString(format).replace("%L", text); } q->setText(text); #endif } }; // --- class KexiWelcomeStatusBar::Private { public: explicit Private(KexiWelcomeStatusBar* _q) : statusWidget(0), helpAction(0), shareAction(0), cancelAction(0), q(_q) { rccFname = findFilename("status.rcc"); if (!rccFname.isEmpty()) { QResource::registerResource(rccFname); } scores.insert(KexiUserFeedbackAgent::BasicArea, 4); scores.insert(KexiUserFeedbackAgent::SystemInfoArea, 4); scores.insert(KexiUserFeedbackAgent::ScreenInfoArea, 2); scores.insert(KexiUserFeedbackAgent::RegionalSettingsArea, 2); totalFeedbackScore = 0; foreach (int s, scores.values()) { totalFeedbackScore += s; } donationScore = 20; donated = false; //qDebug() << "totalFeedbackScore:" << totalFeedbackScore; } ~Private() { delete msgWidget; if (!rccFname.isEmpty()) { QResource::unregisterResource(rccFname); } } int currentFeedbackScore() const { int score = 0; KexiUserFeedbackAgent *f = KexiMainWindowIface::global()->userFeedbackAgent(); KexiUserFeedbackAgent::Areas areas = f->enabledAreas(); for (QMap::ConstIterator it(scores.constBegin()); it!=scores.constEnd(); ++it) { if (areas & it.key()) { score += it.value(); } } //qDebug() << score; return score; } template T widgetOfClass(T parent, const char *widgetName) const { T w = parent->template findChild(widgetName); if (!w) { qWarning() << "NO SUCH widget" << widgetName << "in" << parent; } return w; } QWidget* widget(QWidget *parent, const char *widgetName) const { return widgetOfClass(parent, widgetName); } QObject* object(QObject *parent, const char *objectName) const { QObject *o = parent->findChild(objectName); if (!o) { qWarning() << "NO SUCH object" << objectName << "in" << parent; } return o; } void setProperty(QWidget *parent, const char *widgetName, const char *propertyName, const QVariant &value) { QWidget *w = widget(parent, widgetName); if (w) { w->setProperty(propertyName, value); } } QVariant property(QWidget *parent, const char *widgetName, const char *propertyName) const { QWidget *w = widget(parent, widgetName); return w ? w->property(propertyName) : QVariant(); } void connect(QWidget *parent, const char *widgetName, const char *signalName, QObject *receiver, const char *slotName) { QWidget *w = widget(parent, widgetName); if (w) { QObject::connect(w, signalName, receiver, slotName); } } void animatedHide(QWidget *parent, const char *widgetName) { QWidget *w = widget(parent, widgetName); if (!w) return; KexiFadeWidgetEffect *animation = new KexiFadeWidgetEffect(w); QObject::connect(animation, SIGNAL(destroyed()), w, SLOT(hide())); animation->start(); } QWidget* loadGui(const QString &guiFileName, QWidget *parentWidget = 0) { QString fname = findFilename(guiFileName); if (fname.isEmpty()) { qWarning() << "filename" << fname << "not found"; return 0; } QFile file(fname); if (!file.open(QIODevice::ReadOnly)) { qWarning() << "could not open file" << fname; return 0; } QUiLoader loader; QWidget* widget = loader.load(&file, parentWidget); if (!widget) { qWarning() << "could load ui from file" << fname; } file.close(); return widget; } void updateStatusWidget() { QWidget *widget = loadGui("status.ui", statusScrollArea); if (!widget) { return; } int smallFontSize = qFloor((QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont).pointSizeF() + q->font().pointSizeF()) / 2.0); smallFont = q->font(); smallFont.setPointSizeF(smallFontSize); widget->setFont(smallFont); //delete statusWidget; statusWidget = widget; statusScrollArea->setWidget(statusWidget); setProperty(statusWidget, "contribution_progress", "minimumHeight", q->fontMetrics().height()); setProperty(statusWidget, "contribution_progress", "maximumHeight", q->fontMetrics().height()); label_involved_text_mask = property(statusWidget, "label_involved", "text").toString(); setProperty(statusWidget, "link_share_usage_info", "text", property(statusWidget, "link_share_usage_info", "text").toString().arg(totalFeedbackScore)); link_share_more_usage_info_mask = property(statusWidget, "link_share_more_usage_info", "text").toString(); setProperty(statusWidget, "link_donate", "text", property(statusWidget, "link_donate", "text").toString().arg(donationScore)); updateDonationInfo(); updateUserProgress(); updateContributionLinksVisibility(); // do not alter background palette QPalette pal(widget->palette()); pal.setColor(QPalette::Disabled, QPalette::Base, pal.color(QPalette::Normal, QPalette::Base)); widget->setPalette(pal); connect(statusWidget, "link_contribute_show_help", SIGNAL(linkActivated(QString)), q, SLOT(showContributionHelp())); connect(statusWidget, "link_share_usage_info", SIGNAL(linkActivated(QString)), q, SLOT(showShareUsageInfo())); connect(statusWidget, "link_share_more_usage_info", SIGNAL(linkActivated(QString)), q, SLOT(showShareUsageInfo())); connect(statusWidget, "link_show_contribution_details", SIGNAL(linkActivated(QString)), q, SLOT(showContributionDetails())); setProperty(statusWidget, "donation_url", "visible", false); connect(statusWidget, "link_donate", SIGNAL(linkActivated(QString)), q, SLOT(showDonation())); } void setUserProgress(int progress) { setProperty(statusWidget, "contribution_progress", "value", progress); setProperty(statusWidget, "label_involved", "text", label_involved_text_mask.arg(progress)); } void updateUserProgress() { int progress = 0; progress += currentFeedbackScore(); if (donated) { progress += donationScore; } setUserProgress(progress); } void updateContributionLinksVisibility() { KexiUserFeedbackAgent *f = KexiMainWindowIface::global()->userFeedbackAgent(); int availableLinks = 0; bool noneEnabled = f->enabledAreas() == KexiUserFeedbackAgent::NoAreas; bool allEnabled = f->enabledAreas() == KexiUserFeedbackAgent::AllAreas; setProperty(statusWidget, "share_usage_info", "visible", noneEnabled); if (noneEnabled) { availableLinks++; } setProperty(statusWidget, "share_more_usage_info", "visible", !noneEnabled && !allEnabled); if (!noneEnabled && !allEnabled) { availableLinks++; } setProperty(statusWidget, "link_share_more_usage_info", "text", link_share_more_usage_info_mask.arg(totalFeedbackScore - currentFeedbackScore())); setProperty(statusWidget, "lbl_contribute", "visible", availableLinks > 0); } void updateDonationInfo() { KConfigGroup configGroup(KSharedConfig::openConfig()->group("User Feedback")); QDateTime lastDonation = configGroup.readEntry("LastDonation", QDateTime()); if (lastDonation.isValid()) { int days = lastDonation.secsTo(QDateTime::currentDateTime()) / 60 / 60 / 24; if (days >= DONATION_INTERVAL) { donated = false; qDebug() << "last donation declared" << days << "days ago, next in" << (DONATION_INTERVAL - days) << "days."; } else if (days >= 0) { donated = true; } } //show always: setProperty(statusWidget, "donate", "visible", !donated); } enum CalloutAlignment { AlignToBar, AlignToWidget }; //! Aligns callout pointer position of msgWidget to widget named @a alignToWidgetName void setMessageWidgetCalloutPointerPosition( const QString& alignToWidgetName, CalloutAlignment calloutAlignment = AlignToBar) { //qDebug() << q->pos() << q->mapToGlobal(QPoint(0, 100)); QPoint p(q->mapToGlobal(QPoint(0, 100))); QWidget *alignToWidget = this->widget(statusWidget, alignToWidgetName.toLatin1()); if (alignToWidget) { p.setY( alignToWidget->mapToGlobal( QPoint(-5, alignToWidget->height() / 2)).y()); if (calloutAlignment == AlignToWidget) { p.setX(alignToWidget->mapToGlobal(QPoint(-5, 0)).x()); //qDebug() << p; } } else { qWarning() << alignToWidgetName << "not found!"; } msgWidget->setCalloutPointerPosition(p, alignToWidget); } //! Shows message widget taking maximum space within the welcome page //! Returns created layout for further use into @a layout. //! Created widge is assigned to msgWidget. //! Calls slot @a slotToCallAfterShow after animated showing, if provided. //! Call msgWidget->animatedShow() afterwards. void showMaximizedMessageWidget(const QString &alignToWidgetName, QPointer *layout, const char* slotToCallAfterShow, CalloutAlignment calloutAlignment = AlignToBar) { QWidget *alignToWidget = this->widget(statusWidget, alignToWidgetName.toLatin1()); int msgWidth; if (alignToWidget && calloutAlignment == AlignToWidget) { msgWidth = q->parentWidget()->width() - alignToWidget->width() - 10; } else { msgWidth = q->parentWidget()->width() - q->width(); } QWidget *widget = new QWidget; *layout = new QGridLayout(widget); if (msgWidth > 100) { // nice text margin (*layout)->setColumnMinimumWidth(0, 50); } //qDebug() << (q->parentWidget()->width() - q->width()) << "***"; KexiContextMessage msg(widget); if (msgWidget) { delete static_cast(msgWidget); } msgWidget = new KexiContextMessageWidget(q->parentWidget()->parentWidget(), 0, 0, msg); msgWidget->setCalloutPointerDirection(KMessageWidget::Right); msgWidget->setMessageType(KMessageWidget::Information); msgWidget->setCloseButtonVisible(true); int offset_y = 0; if (alignToWidget) { offset_y = alignToWidget->mapToGlobal(QPoint(0, 0)).y() - q->parentWidget()->mapToGlobal(QPoint(0, 0)).y(); } else { qWarning() << alignToWidgetName << "not found!"; } msgWidget->resize(msgWidth, q->parentWidget()->height() - offset_y); setMessageWidgetCalloutPointerPosition(alignToWidgetName, calloutAlignment); msgWidget->setResizeTrackingPolicy(Qt::Horizontal | Qt::Vertical); statusScrollArea->setEnabled(false); // async show to for speed up if (slotToCallAfterShow) { QObject::connect(msgWidget, SIGNAL(animatedShowFinished()), q, slotToCallAfterShow); } QObject::connect(msgWidget, SIGNAL(animatedHideFinished()), q, SLOT(slotMessageWidgetClosed())); } ScrollArea *statusScrollArea; QWidget *statusWidget; QVBoxLayout *lyr; QPointer msgWidget; QFont smallFont; QAction *helpAction; QAction *shareAction; QAction *cancelAction; QString label_involved_text_mask; QString link_share_more_usage_info_mask; QPointer contributionHelpLayout; QPointer contributionDetailsLayout; QPointer contributionDetailsWidget; QMap scores; QString countryMask; QString languageMask; bool detailsDataVisible; int totalFeedbackScore; int donationScore; bool donated; KexiWelcomeStatusBarGuiUpdater guiUpdater; private: QString rccFname; KexiWelcomeStatusBar *q; QMap dict; }; KexiWelcomeStatusBar::KexiWelcomeStatusBar(QWidget* parent) : QWidget(parent), d(new Private(this)) { d->lyr = new QVBoxLayout(this); init(); } KexiWelcomeStatusBar::~KexiWelcomeStatusBar() { delete d; } void KexiWelcomeStatusBar::init() { d->statusScrollArea = new ScrollArea(this); d->lyr->addWidget(d->statusScrollArea); d->updateStatusWidget(); QTimer::singleShot(10, &d->guiUpdater, SLOT(update())); } void KexiWelcomeStatusBar::showContributionHelp() { d->showMaximizedMessageWidget("link_contribute_show_help", &d->contributionHelpLayout, SLOT(slotShowContributionHelpContents())); d->msgWidget->animatedShow(); } void KexiWelcomeStatusBar::slotShowContributionHelpContents() { QWidget *helpWidget = d->loadGui("contribution_help.ui"); d->contributionHelpLayout->addWidget(helpWidget, 1, 1); d->msgWidget->setPaletteInherited(); } void KexiWelcomeStatusBar::slotMessageWidgetClosed() { d->statusScrollArea->setEnabled(true); d->updateDonationInfo(); d->updateUserProgress(); d->updateContributionLinksVisibility(); } void KexiWelcomeStatusBar::showShareUsageInfo() { if (!sender()) { return; } QWidget *widget = d->loadGui("status_strings.ui"); if (!widget) { return; } QLabel *lbl = widget->findChild("question"); if (!lbl) { return; } KexiContextMessage msg(lbl->text()); delete widget; if (!d->helpAction) { d->helpAction = new QAction(KStandardGuiItem::help().icon(), KStandardGuiItem::help().text(), this); connect(d->helpAction, SIGNAL(triggered()), this, SLOT(showContributionHelp())); } if (!d->shareAction) { d->shareAction = new QAction(KStandardGuiItem::yes().icon(), xi18n("Share"), this); connect(d->shareAction, SIGNAL(triggered()), this, SLOT(slotShareFeedback())); } if (!d->cancelAction) { d->cancelAction = new QAction(KStandardGuiItem::cancel().icon(), KStandardGuiItem::cancel().text(), this); QObject::connect(d->cancelAction, SIGNAL(triggered()), this, SLOT(slotCancelled())); } msg.addAction(d->helpAction, KexiContextMessage::AlignLeft); msg.addAction(d->shareAction); msg.addAction(d->cancelAction); if (d->msgWidget) { delete static_cast(d->msgWidget); } d->msgWidget = new KexiContextMessageWidget(parentWidget(), 0, 0, msg); d->msgWidget->setMessageType(KMessageWidget::Information); d->msgWidget->setCalloutPointerDirection(KMessageWidget::Right); d->setMessageWidgetCalloutPointerPosition(sender()->objectName()); d->statusScrollArea->setEnabled(false); d->msgWidget->setMaximumWidth(parentWidget()->width() - width()); d->msgWidget->setResizeTrackingPolicy(Qt::Horizontal); d->msgWidget->animatedShow(); } void KexiWelcomeStatusBar::showDonation() { if (!sender()) { return; } if (KMessageBox::Yes != KMessageBox::questionYesNo(this, xi18nc("@info donate to the project", "Kexi may be totally free, but its development is costly." "Power, hardware, office space, internet access, traveling for meetings - everything costs." "Direct donation is the easiest and fastest way to efficiently support the Kexi Project. " "Everyone, regardless of any degree of involvement can do so." "What do you receive for your donation? Kexi will become more feature-full and stable as " "contributors will be able to devote more time to Kexi. Not only you can " "expect new features, but you can also have an influence on what features are added!" "Currently we are accepting donations through BountySource (a funding platform " "for open-source software) using secure PayPal, Bitcoin and Google Wallet transfers." "Contact us at http://community.kde.org/Kexi/Contact for more information." "Thanks for your support!"), xi18n("Donate to the Project"), KGuiItem(xi18nc("@action:button Go to Donation", "Proceed to the Donation Web Page"), QIcon(":/icons/heart.png")), KGuiItem(xi18nc("Do not donate now", "Not Now")))) { return; } QUrl donationUrl(d->property(this, "donation_url", "text").toString()); if (donationUrl.isValid()) { QDesktopServices::openUrl(donationUrl); d->donated = true; d->updateStatusWidget(); KConfigGroup configGroup(KSharedConfig::openConfig()->group("User Feedback")); int donationsCount = configGroup.readEntry("DonationsCount", 0); configGroup.writeEntry("LastDonation", QDateTime::currentDateTime()); configGroup.writeEntry("DonationsCount", donationsCount + 1); } else { qWarning() << "Invalid donation URL" << donationUrl; } } void KexiWelcomeStatusBar::slotShareFeedback() { d->statusScrollArea->setEnabled(true); d->msgWidget->animatedHide(); KexiMainWindowIface::global()->userFeedbackAgent() ->setEnabledAreas(KexiUserFeedbackAgent::AllAreas); d->animatedHide(d->statusWidget, "share_usage_info"); d->animatedHide(d->statusWidget, "share_more_usage_info"); d->animatedHide(d->statusWidget, "lbl_contribute"); d->updateUserProgress(); } void KexiWelcomeStatusBar::slotCancelled() { d->statusScrollArea->setEnabled(true); } // Contribution Details BEGIN void KexiWelcomeStatusBar::showContributionDetails() { d->showMaximizedMessageWidget("link_show_contribution_details", &d->contributionDetailsLayout, 0, KexiWelcomeStatusBar::Private::AlignToWidget); d->contributionDetailsLayout->setColumnMinimumWidth(0, 6); // smaller d->contributionDetailsWidget = d->loadGui("contribution_details.ui"); KexiUserFeedbackAgent *f = KexiMainWindowIface::global()->userFeedbackAgent(); d->setProperty(d->contributionDetailsWidget, "group_share", "checked", f->enabledAreas() != KexiUserFeedbackAgent::NoAreas); d->setProperty(d->contributionDetailsWidget, "group_basic", "title", d->property(d->contributionDetailsWidget, "group_basic", "title") .toString().arg(d->scores.value(KexiUserFeedbackAgent::BasicArea))); updateContributionGroupCheckboxes(); d->setProperty(d->contributionDetailsWidget, "group_system", "title", d->property(d->contributionDetailsWidget, "group_system", "title") .toString().arg(d->scores.value(KexiUserFeedbackAgent::SystemInfoArea))); d->connect(d->contributionDetailsWidget, "group_system", SIGNAL(toggled(bool)), this, SLOT(slotShareContributionDetailsGroupToggled(bool))); d->setProperty(d->contributionDetailsWidget, "group_screen", "title", d->property(d->contributionDetailsWidget, "group_screen", "title") .toString().arg(d->scores.value(KexiUserFeedbackAgent::ScreenInfoArea))); d->connect(d->contributionDetailsWidget, "group_screen", SIGNAL(toggled(bool)), this, SLOT(slotShareContributionDetailsGroupToggled(bool))); d->setProperty(d->contributionDetailsWidget, "group_regional_settings", "title", d->property(d->contributionDetailsWidget, "group_regional_settings", "title") .toString().arg(d->scores.value(KexiUserFeedbackAgent::RegionalSettingsArea))); d->connect(d->contributionDetailsWidget, "group_regional_settings", SIGNAL(toggled(bool)), this, SLOT(slotShareContributionDetailsGroupToggled(bool))); d->detailsDataVisible = false; slotShareContributionDetailsToggled( d->property(d->contributionDetailsWidget, "group_share", "checked").toBool()); d->detailsDataVisible = true; // to switch off slotToggleContributionDetailsDataVisibility(); d->connect(d->contributionDetailsWidget, "group_share", SIGNAL(toggled(bool)), this, SLOT(slotShareContributionDetailsToggled(bool))); d->connect(d->contributionDetailsWidget, "link_show_shared_info", SIGNAL(linkActivated(QString)), this, SLOT(slotToggleContributionDetailsDataVisibility())); d->setProperty(d->contributionDetailsWidget, "label_where_is_info_sent", "visible", false); ScrollArea *contributionDetailsArea = new ScrollArea(d->msgWidget); d->contributionDetailsLayout->addWidget(contributionDetailsArea, 1, 1); contributionDetailsArea->setWidget(d->contributionDetailsWidget); d->msgWidget->animatedShow(); d->msgWidget->setPaletteInherited(); } void KexiWelcomeStatusBar::updateContributionGroupCheckboxes() { KexiUserFeedbackAgent *f = KexiMainWindowIface::global()->userFeedbackAgent(); d->setProperty(d->contributionDetailsWidget, "group_system", "checked", bool(f->enabledAreas() & KexiUserFeedbackAgent::SystemInfoArea)); d->setProperty(d->contributionDetailsWidget, "group_screen", "checked", bool(f->enabledAreas() & KexiUserFeedbackAgent::ScreenInfoArea)); d->setProperty(d->contributionDetailsWidget, "group_regional_settings", "checked", bool(f->enabledAreas() & KexiUserFeedbackAgent::RegionalSettingsArea)); } void KexiWelcomeStatusBar::slotShareContributionDetailsToggled(bool on) { //qDebug() << sender(); QWidget* group_share = d->widget(d->contributionDetailsWidget, "group_share"); KexiUserFeedbackAgent *f = KexiMainWindowIface::global()->userFeedbackAgent(); if (sender() == group_share) { f->setEnabledAreas(on ? KexiUserFeedbackAgent::AllAreas : KexiUserFeedbackAgent::NoAreas); updateContributionGroupCheckboxes(); } if (!group_share) { return; } for (int i=0; i < group_share->layout()->count(); i++) { QWidget *w = group_share->layout()->itemAt(i)->widget(); if (w) { w->setVisible(on); } } if (d->detailsDataVisible) { slotToggleContributionDetailsDataVisibility(); } // fill shared values QLocale locale; foreach(QLabel* lbl, d->contributionDetailsWidget->findChildren()) { if (lbl->objectName().startsWith(QLatin1String("value_"))) { QString name = lbl->objectName().mid(6); // cut "value_" QVariant value; if (name == QLatin1String("screen_size")) { value = QString("%1 x %2").arg(f->value("screen_width").toString()) .arg(f->value("screen_height").toString()); } else if (name == QLatin1String("country")) { if (d->countryMask.isEmpty()) { d->countryMask = lbl->text(); } value = d->countryMask .arg(f->value(name).toString() /*!< @todo KEXI3 port KLocale::global()->countryCodeToName(f->value(name).toString()) */) .arg(f->value(name).toString()); } else if (name == QLatin1String("language")) { if (d->languageMask.isEmpty()) { d->languageMask = lbl->text(); } value = d->languageMask .arg(f->value(name).toString() /*!< @todo KEXI3 port KLocale::global()->languageCodeToName(f->value(name).toString()) */) .arg(f->value(name).toString()); } else { value = f->value(name); } if (value.type() == QVariant::Bool) { value = value.toBool() ? KStandardGuiItem::yes().plainText() : KStandardGuiItem::no().plainText(); } if (!value.isNull()) { lbl->setText(value.toString()); } } else if (lbl->objectName().startsWith(QLatin1String("desc_"))) { lbl->setFont(d->smallFont); } } QLabel* lbl; KConfigGroup configGroup(KSharedConfig::openConfig()->group("User Feedback")); if ((lbl = d->contributionDetailsWidget->findChild("value_recent_donation"))) { QDateTime lastDonation = configGroup.readEntry("LastDonation", QDateTime()); QString recentDonation = "-"; if (lastDonation.isValid()) { int days = lastDonation.secsTo(QDateTime::currentDateTime()) / 60 / 60 / 24; if (days == 0) { recentDonation = xi18nc("Donation today", "today"); } else if (days > 0) { recentDonation = xi18ncp("Recent donation date (xx days)", "%1 (1 day)", "%1 (%2 days)", locale.toString(lastDonation), days); } } lbl->setText(recentDonation); } if ((lbl = d->contributionDetailsWidget->findChild("value_donations_count"))) { int donationsCount = configGroup.readEntry("DonationsCount", 0); if (donationsCount == 0) { lbl->setText(QString::number(donationsCount)); } else { lbl->setText(xi18nc("donations count", "%1 (thanks!)", donationsCount)); } } } static void setArea(KexiUserFeedbackAgent::Areas *areas, KexiUserFeedbackAgent::Area area, bool on) { *areas |= area; if (!on) { *areas ^= area; } } void KexiWelcomeStatusBar::slotShareContributionDetailsGroupToggled(bool on) { if (!sender()) { return; } const QString name = sender()->objectName(); KexiUserFeedbackAgent *f = KexiMainWindowIface::global()->userFeedbackAgent(); KexiUserFeedbackAgent::Areas areas = f->enabledAreas(); //qDebug() << areas; if (name == "group_system") { setArea(&areas, KexiUserFeedbackAgent::SystemInfoArea, on); } else if (name == "group_screen") { setArea(&areas, KexiUserFeedbackAgent::ScreenInfoArea, on); } else if (name == "group_regional_settings") { setArea(&areas, KexiUserFeedbackAgent::RegionalSettingsArea, on); } if (areas) { areas |= KexiUserFeedbackAgent::AnonymousIdentificationArea; } f->setEnabledAreas(areas); //qDebug() << f->enabledAreas(); } void KexiWelcomeStatusBar::slotToggleContributionDetailsDataVisibility() { QWidget* value_app_ver = d->widget(d->contributionDetailsWidget, "value_app_ver"); if (!value_app_ver) { return; } d->detailsDataVisible = !d->detailsDataVisible; if (d->detailsDataVisible) { d->setProperty(d->contributionDetailsWidget, "link_show_shared_info", "visible", false); d->setProperty(d->contributionDetailsWidget, "label_where_is_info_sent", "visible", true); } bool show = d->contributionDetailsWidget->isVisible(); QList list; d->contributionDetailsWidget->hide(); QWidget* group_basic = d->widget(d->contributionDetailsWidget, "group_basic"); if (group_basic) { list += group_basic->findChildren(); } QWidget* group_system = d->widget(d->contributionDetailsWidget, "group_system"); if (group_system) { list += group_system->findChildren(); } QWidget* group_screen = d->widget(d->contributionDetailsWidget, "group_screen"); if (group_screen) { list += group_screen->findChildren(); } QWidget* group_regional_settings = d->widget(d->contributionDetailsWidget, "group_regional_settings"); if (group_regional_settings) { list += group_regional_settings->findChildren(); } foreach (QWidget* w, list) { if (qobject_cast(w) && !w->objectName().startsWith(QLatin1String("desc_"))) { //qDebug() << "+++" << w; w->setVisible(d->detailsDataVisible); } } if (show) { d->contributionDetailsWidget->show(); } } // Contribution Details END diff --git a/src/plugins/importexport/csv/kexicsvimportdialog.cpp b/src/plugins/importexport/csv/kexicsvimportdialog.cpp index 0996abe13..86363b35b 100644 --- a/src/plugins/importexport/csv/kexicsvimportdialog.cpp +++ b/src/plugins/importexport/csv/kexicsvimportdialog.cpp @@ -1,2185 +1,2188 @@ /* This file is part of the KDE project Copyright (C) 2005-2016 Jarosław Staniek Copyright (C) 2012 Oleg Kukharchuk This work is based on kspread/dialogs/kspread_dlg_csv.cc and could be merged back with Calligra Libraries. Copyright (C) 2002-2003 Norbert Andres Copyright (C) 2002-2003 Ariya Hidayat Copyright (C) 2002 Laurent Montel Copyright (C) 1999 David Faure This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "kexicsvimportdialog.h" #include "KexiCSVImportDialogModel.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kexicsvwidgets.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define _IMPORT_ICON koIconNeededWithSubs("change to file_import or so", "file_import","table") //! @internal An item delegate for KexiCSVImportDialog's table view class KexiCSVImportDialogItemDelegate : public QStyledItemDelegate { public: KexiCSVImportDialogItemDelegate(QObject *parent = 0); virtual QWidget* createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const; }; KexiCSVImportDialogItemDelegate::KexiCSVImportDialogItemDelegate(QObject *parent) : QStyledItemDelegate(parent) { } QWidget* KexiCSVImportDialogItemDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const { QStyleOptionViewItem newOption(option); QWidget *editor = QStyledItemDelegate::createEditor(parent, newOption, index); if (editor && index.row() == 0) { QFont f(editor->font()); f.setBold(true); editor->setFont(f); } return editor; } // -- //! @internal class KexiCSVImportStatic { public: KexiCSVImportStatic() : types(QVector() << KDbField::Text << KDbField::Integer << KDbField::Double << KDbField::Boolean << KDbField::Date << KDbField::Time << KDbField::DateTime) { typeNames.insert(KDbField::Text, KDbField::typeGroupName(KDbField::TextGroup)); typeNames.insert(KDbField::Integer, KDbField::typeGroupName(KDbField::IntegerGroup)); typeNames.insert(KDbField::Double, KDbField::typeGroupName(KDbField::FloatGroup)); typeNames.insert(KDbField::Boolean, KDbField::typeName(KDbField::Boolean)); typeNames.insert(KDbField::Date, KDbField::typeName(KDbField::Date)); typeNames.insert(KDbField::Time, KDbField::typeName(KDbField::Time)); typeNames.insert(KDbField::DateTime, KDbField::typeName(KDbField::DateTime)); for (int i = 0; i < types.size(); ++i) { indicesForTypes.insert(types[i], i); } } const QVector types; QHash typeNames; QHash indicesForTypes; }; Q_GLOBAL_STATIC(KexiCSVImportStatic, kexiCSVImportStatic) #define MAX_ROWS_TO_PREVIEW 100 //max 100 rows is reasonable #define MAX_BYTES_TO_PREVIEW 10240 //max 10KB is reasonable #define MAX_CHARS_TO_SCAN_WHILE_DETECTING_DELIMITER 4096 #define MINIMUM_YEAR_FOR_100_YEAR_SLIDING_WINDOW 1930 #define PROGRESS_STEP_MS (1000/5) // 5 updates per second static bool shouldSaveRow(int row, bool firstRowForFieldNames) { return row > (firstRowForFieldNames ? 1 : 0); } // -- class KexiCSVImportDialog::Private { public: Private() : imported(false) { } ~Private() { qDeleteAll(m_uniquenessTest); } void clearDetectedTypes() { m_detectedTypes.clear(); } void clearUniquenessTests() { qDeleteAll(m_uniquenessTest); m_uniquenessTest.clear(); } KDbField::Type detectedType(int col) const { return m_detectedTypes.value(col, KDbField::InvalidType); } void setDetectedType(int col, KDbField::Type type) { if (m_detectedTypes.count() <= col) { for (int i = m_detectedTypes.count(); i < col; ++i) { // append missing bits m_detectedTypes.append(KDbField::InvalidType); } m_detectedTypes.append(type); } else { m_detectedTypes[col] = type; } } QList* uniquenessTest(int col) const { return m_uniquenessTest.value(col); } void setUniquenessTest(int col, QList* test) { if (m_uniquenessTest.count() <= col) { for (int i = m_uniquenessTest.count(); i < col; ++i) { // append missing bits m_uniquenessTest.append(0); } m_uniquenessTest.append(test); } else { m_uniquenessTest[col] = test; } } bool imported; private: //! vector of detected types //! @todo more types QList m_detectedTypes; //! m_detectedUniqueColumns[i]==true means that i-th column has unique values //! (only for numeric type) QList< QList* > m_uniquenessTest; }; // -- KexiCSVImportDialog::KexiCSVImportDialog(Mode mode, QWidget * parent) : KAssistantDialog(parent), m_parseComments(false), m_canceled(false), m_adjustRows(true), m_startline(0), m_textquote(QString(KEXICSV_DEFAULT_FILE_TEXT_QUOTE)[0]), m_commentSymbol(QString(KEXICSV_DEFAULT_COMMENT_START)[0]), m_mode(mode), m_columnsAdjusted(false), m_firstFillTableCall(true), m_blockUserEvents(false), m_primaryKeyColumn(-1), m_dialogCanceled(false), m_conn(0), m_fieldsListModel(0), m_destinationTableSchema(0), m_implicitPrimaryKeyAdded(false), m_allRowsLoadedInPreview(false), m_stoppedAt_MAX_BYTES_TO_PREVIEW(false), m_stringNo("no"), m_stringI18nNo(xi18n("no")), m_stringFalse("false"), m_stringI18nFalse(xi18n("false")), m_newTable(false), m_partItemForSavedTable(0), m_importInProgress(false), m_importCanceled(false), d(new Private) { setWindowTitle( mode == File ? xi18nc("@title:window", "Import CSV Data From File") : xi18nc("@title:window", "Paste CSV Data From Clipboard") ); setWindowIcon(_IMPORT_ICON); //! @todo use "Paste CSV Data From Clipboard" caption for mode==Clipboard setObjectName("KexiCSVImportDialog"); setSizeGripEnabled(true); KexiMainWindowIface::global()->setReasonableDialogSize(this); KGuiItem::assign(configureButton(), KStandardGuiItem::configure()); finishButton()->setEnabled(false); backButton()->setEnabled(false); KConfigGroup importExportGroup(KSharedConfig::openConfig()->group("ImportExport")); m_maximumRowsForPreview = importExportGroup.readEntry( "MaximumRowsForPreviewInImportDialog", MAX_ROWS_TO_PREVIEW); m_maximumBytesForPreview = importExportGroup.readEntry( "MaximumBytesForPreviewInImportDialog", MAX_BYTES_TO_PREVIEW); m_minimumYearFor100YearSlidingWindow = importExportGroup.readEntry( "MinimumYearFor100YearSlidingWindow", MINIMUM_YEAR_FOR_100_YEAR_SLIDING_WINDOW); m_pkIcon = SmallIcon(KexiIconName("database-key")); if (m_mode == File) { createFileOpenPage(); } else if (m_mode == Clipboard) { QString subtype("plain"); m_clipboardData = QApplication::clipboard()->text(subtype, QClipboard::Clipboard); /* debug for (int i=0;QApplication::clipboard()->data(QClipboard::Clipboard)->format(i);i++) qDebug() << i << ": " << QApplication::clipboard()->data(QClipboard::Clipboard)->format(i); */ } else { return; } m_file = 0; m_inputStream = 0; createOptionsPage(); createImportMethodPage(); createTableNamePage(); createImportPage(); /** @todo reuse Clipboard too! */ /*if ( m_mode == Clipboard ) { setWindowTitle( xi18n( "Inserting From Clipboard" ) ); QMimeSource * mime = QApplication::clipboard()->data(); if ( !mime ) { KMessageBox::information( this, xi18n("There is no data in the clipboard.") ); m_canceled = true; return; } if ( !mime->provides( "text/plain" ) ) { KMessageBox::information( this, xi18n("There is no usable data in the clipboard.") ); m_canceled = true; return; } m_fileArray = QByteArray(mime->encodedData( "text/plain" ) ); } else if ( mode == File ) {*/ - m_dateRegExp = QRegExp("(\\d{1,4})([/\\-\\.])(\\d{1,2})([/\\-\\.])(\\d{1,4})"); - m_timeRegExp1 = QRegExp("(\\d{1,2}):(\\d{1,2}):(\\d{1,2})"); - m_timeRegExp2 = QRegExp("(\\d{1,2}):(\\d{1,2})"); - m_fpNumberRegExp1 = QRegExp("[\\-]{0,1}\\d*[,\\.]\\d+"); + m_dateRegExp = QRegularExpression("^(\\d{1,4})([/\\-\\.])(\\d{1,2})([/\\-\\.])(\\d{1,4})$"); + m_timeRegExp1 = QRegularExpression("^(\\d{1,2}):(\\d{1,2}):(\\d{1,2})$"); + m_timeRegExp2 = QRegularExpression("^(\\d{1,2}):(\\d{1,2})$"); + m_fpNumberRegExp1 = QRegularExpression("^[\\-]{0,1}\\d*[,\\.]\\d+$"); // E notation, e.g. 0.1e2, 0.1e+2, 0.1e-2, 0.1E2, 0.1E+2, 0.1E-2 - m_fpNumberRegExp2 = QRegExp("[\\-]{0,1}\\d*[,\\.]\\d+[Ee][+-]{0,1}\\d+"); + m_fpNumberRegExp2 = QRegularExpression("^[\\-]{0,1}\\d*[,\\.]\\d+[Ee][+-]{0,1}\\d+$"); m_loadingProgressDlg = 0; if (m_mode == Clipboard) { m_infoLbl->setIcon(koIconName("edit-paste")); } m_tableView->setSelectionMode(QAbstractItemView::SingleSelection); connect(m_formatCombo, SIGNAL(activated(int)), this, SLOT(formatChanged(int))); connect(m_delimiterWidget, SIGNAL(delimiterChanged(QString)), this, SLOT(delimiterChanged(QString))); connect(m_commentWidget, SIGNAL(commentSymbolChanged(QString)), this, SLOT(commentSymbolChanged(QString))); connect(m_startAtLineSpinBox, SIGNAL(valueChanged(int)), this, SLOT(startlineSelected(int))); connect(m_comboQuote, SIGNAL(activated(int)), this, SLOT(textquoteSelected(int))); connect(m_tableView->selectionModel(), SIGNAL(currentChanged(QModelIndex,QModelIndex)), this, SLOT(currentCellChanged(QModelIndex,QModelIndex))); connect(m_ignoreDuplicates, SIGNAL(stateChanged(int)), this, SLOT(ignoreDuplicatesChanged(int))); connect(m_1stRowForFieldNames, SIGNAL(stateChanged(int)), this, SLOT(slot1stRowForFieldNamesChanged(int))); connect(configureButton(), &QPushButton::clicked, this, &KexiCSVImportDialog::optionsButtonClicked); connect(this, SIGNAL(currentPageChanged(KPageWidgetItem*,KPageWidgetItem*)), this, SLOT(slotCurrentPageChanged(KPageWidgetItem*,KPageWidgetItem*))); KexiUtils::installRecursiveEventFilter(this, this); if ( m_mode == Clipboard ) initLater(); } KexiCSVImportDialog::~KexiCSVImportDialog() { delete m_file; delete m_inputStream; delete d; } void KexiCSVImportDialog::next() { KPageWidgetItem *curPage = currentPage(); if (curPage == m_openFilePage) { m_fname = m_openFileWidget->highlightedFile(); if (m_openFileWidget->checkSelectedFile()) { m_fname = m_openFileWidget->highlightedFile(); } else { return; } if (m_fname.isEmpty()) { KMessageBox::sorry(this, xi18nc("@info", "Select source filename.")); return; } if (!openData()) { return; } } else if (curPage == m_optionsPage) { const int numRows(m_table->rowCount()); if (numRows == 0) return; //impossible if (numRows == 1) { if (KMessageBox::No == KMessageBox::questionYesNo(this, xi18n("Data set contains no rows. Do you want to import empty table?"))) return; } } else if (curPage == m_tableNamePage) { KexiGUIMessageHandler msg; KexiProject *project = KexiMainWindowIface::global()->project(); if (!project) { msg.showErrorMessage(KDbMessageHandler::Error, xi18n("No project available.")); return; } m_conn = project->dbConnection(); if (!m_conn) { msg.showErrorMessage(KDbMessageHandler::Error, xi18n("No database connection available.")); return; } if (m_newTable) { m_partItemForSavedTable->setCaption(m_newTableWidget->captionText()); m_partItemForSavedTable->setName(m_newTableWidget->nameText()); KexiPart::Part *part = Kexi::partManager().partForPluginId("org.kexi-project.table"); KDbObject tmp; tristate res = (part && part->info()) ? m_conn->loadObjectData( project->typeIdForPluginId(part->info()->pluginId()), m_newTableWidget->nameText(), &tmp) : false; if (res == true) { KMessageBox::information(this, "

" + part->i18nMessage("Object %1 already exists.", 0) .subs(m_newTableWidget->nameText()).toString() + "

" + xi18n("Please choose other name.") + "

" ); return; } else if (res == false) { qFatal("Plugin org.kexi-project.table not found"); return; } } else { m_partItemForSavedTable = m_tablesList->selectedPartItem(); } } KAssistantDialog::next(); } void KexiCSVImportDialog::slotShowSchema(KexiPart::Item *item) { if (!item) { return; } nextButton()->setEnabled(true); KDbTableOrQuerySchema *tableOrQuery = new KDbTableOrQuerySchema( KexiMainWindowIface::global()->project()->dbConnection(), item->identifier() ); m_tableCaptionLabel->setText(tableOrQuery->captionOrName()); m_tableNameLabel->setText(tableOrQuery->name()); m_recordCountLabel->setText(QString::number(KDb::recordCount(tableOrQuery))); m_colCountLabel->setText(QString::number(tableOrQuery->fieldCount())); delete m_fieldsListModel; m_fieldsListModel = new KexiFieldListModel(m_fieldsListView, ShowDataTypes); m_fieldsListModel->setSchema(tableOrQuery); m_fieldsListView->setModel(m_fieldsListModel); m_fieldsListView->header()->resizeSections(QHeaderView::ResizeToContents); } void KexiCSVImportDialog::slotCurrentPageChanged(KPageWidgetItem *page, KPageWidgetItem *prev) { nextButton()->setEnabled(page == m_saveMethodPage ? false : true); finishButton()->setEnabled(page == m_importPage ? true : false); if (page == m_importPage) { KGuiItem::assign(finishButton(), KGuiItem(xi18nc("@action:button Import CSV", "&Import..."), _IMPORT_ICON)); } configureButton()->setEnabled(page == m_optionsPage); nextButton()->setEnabled(page == m_importPage ? false : true); backButton()->setEnabled(page == m_openFilePage ? false : true); if (page == m_saveMethodPage && prev == m_tableNamePage && m_partItemForSavedTable) { if (m_newTable) { KexiMainWindowIface::global()->project()->deleteUnstoredItem(m_partItemForSavedTable); } m_partItemForSavedTable = 0; } if(page == m_optionsPage){ if (m_mode == File) { m_loadingProgressDlg = new QProgressDialog(this); m_loadingProgressDlg->setObjectName("m_loadingProgressDlg"); m_loadingProgressDlg->setLabelText( xi18nc("@info", "Loading CSV Data from %1...", QDir::toNativeSeparators(m_fname))); m_loadingProgressDlg->setWindowTitle(xi18nc("@title:window", "Loading CSV Data")); m_loadingProgressDlg->setModal(true); m_loadingProgressDlg->setMaximum(m_maximumRowsForPreview); m_loadingProgressDlg->show(); } // delimiterChanged(detectedDelimiter); // this will cause fillTable() m_detectDelimiter = true; m_columnsAdjusted = false; fillTable(); delete m_loadingProgressDlg; m_loadingProgressDlg = 0; if (m_dialogCanceled) { // m_loadingProgressDlg->hide(); // m_loadingProgressDlg->close(); QTimer::singleShot(0, this, SLOT(reject())); return; } currentCellChanged(m_table->index(0,0), QModelIndex()); if (m_loadingProgressDlg) m_loadingProgressDlg->hide(); m_tableView->setFocus(); } else if (page == m_saveMethodPage) { m_newTableButton->setFocus(); } else if (page == m_tableNamePage) { if (m_newTable && !m_partItemForSavedTable) { KexiGUIMessageHandler msg; KexiProject *project = KexiMainWindowIface::global()->project(); //get suggested name based on the file name QString suggestedName; if (m_mode == File) { suggestedName = QUrl(m_fname).fileName(); //remove extension if (!suggestedName.isEmpty()) { const int idx = suggestedName.lastIndexOf('.'); if (idx != -1) { suggestedName = suggestedName.mid(0, idx).simplified(); } } } KexiPart::Part *part = Kexi::partManager().partForPluginId("org.kexi-project.table"); if (!part) { msg.showErrorMessage(Kexi::partManager().result()); return; } //-new part item m_partItemForSavedTable = project->createPartItem(part->info(), suggestedName); if (!m_partItemForSavedTable) { msg.showErrorMessage(project->result()); return; } m_newTableWidget->setCaptionText(m_partItemForSavedTable->caption()); m_newTableWidget->setNameText(m_partItemForSavedTable->name()); m_newTableWidget->captionLineEdit()->setFocus(); m_newTableWidget->captionLineEdit()->selectAll(); } else if (!m_newTable) { KexiPart::Item *i = m_tablesList->selectedPartItem(); if (!i) { nextButton()->setEnabled(false); } slotShowSchema(i); } } else if (page == m_importPage) { m_fromLabel->setFileName(m_fname); m_toLabel->setFileNameText(m_partItemForSavedTable->name()); m_importingProgressBar->hide(); m_importProgressLabel->hide(); } } void KexiCSVImportDialog::createFileOpenPage() { m_openFileWidget = new KexiFileWidget( QUrl("kfiledialog:///CSVImportExport"), //startDir KexiFileWidget::Custom | KexiFileWidget::Opening, this); m_openFileWidget->setObjectName("m_openFileWidget"); m_openFileWidget->setAdditionalFilters(csvMimeTypes().toSet()); m_openFileWidget->setDefaultExtension("csv"); connect(m_openFileWidget, SIGNAL(fileSelected(QUrl)), this, SLOT(next())); m_openFilePage = new KPageWidgetItem(m_openFileWidget, xi18n("Select Import Filename")); addPage(m_openFilePage); } void KexiCSVImportDialog::createOptionsPage() { QWidget *m_optionsWidget = new QWidget(this); QVBoxLayout *lyr = new QVBoxLayout(m_optionsWidget); m_infoLbl = new KexiCSVInfoLabel( m_mode == File ? xi18n("Preview of data from file:") : xi18n("Preview of data from clipboard"), m_optionsWidget, m_mode == File /*showFnameLine*/ ); lyr->addWidget(m_infoLbl); QWidget* page = new QFrame(m_optionsWidget); QGridLayout *glyr = new QGridLayout(page); lyr->addWidget(page); // Delimiter: comma, semicolon, tab, space, other m_delimiterWidget = new KexiCSVDelimiterWidget(true /*lineEditOnBottom*/, page); glyr->addWidget(m_delimiterWidget, 1, 0, 2, 1); QLabel *delimiterLabel = new QLabel(xi18n("Delimiter:"), page); delimiterLabel->setBuddy(m_delimiterWidget); delimiterLabel->setAlignment(Qt::AlignLeft | Qt::AlignBottom); glyr->addWidget(delimiterLabel, 0, 0, 1, 1); m_commentWidget = new KexiCSVCommentWidget(true, page); glyr->addWidget(m_commentWidget, 1, 4); QLabel *commentLabel = new QLabel(xi18n("Comment symbol:"), page); commentLabel->setBuddy(m_commentWidget); commentLabel->setAlignment(Qt::AlignLeft | Qt::AlignBottom); glyr->addWidget(commentLabel, 0, 4); // Format: number, text... //! @todo Object and Currency types m_formatCombo = new KComboBox(page); m_formatCombo->setObjectName("m_formatCombo"); for (int i = 0; i < kexiCSVImportStatic->types.size(); ++i) { m_formatCombo->addItem(kexiCSVImportStatic->typeNames.value(kexiCSVImportStatic->types[i])); } glyr->addWidget(m_formatCombo, 1, 1, 1, 1); m_formatLabel = new QLabel(page); m_formatLabel->setBuddy(m_formatCombo); m_formatLabel->setAlignment(Qt::AlignLeft | Qt::AlignBottom); glyr->addWidget(m_formatLabel, 0, 1); m_primaryKeyField = new QCheckBox(xi18n("Primary key"), page); m_primaryKeyField->setObjectName("m_primaryKeyField"); glyr->addWidget(m_primaryKeyField, 2, 1); connect(m_primaryKeyField, SIGNAL(toggled(bool)), this, SLOT(slotPrimaryKeyFieldToggled(bool))); m_comboQuote = new KexiCSVTextQuoteComboBox(page); glyr->addWidget(m_comboQuote, 1, 2); TextLabel2 = new QLabel(xi18n("Text quote:"), page); TextLabel2->setBuddy(m_comboQuote); TextLabel2->setObjectName("TextLabel2"); TextLabel2->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred); TextLabel2->setAlignment(Qt::AlignLeft | Qt::AlignBottom); glyr->addWidget(TextLabel2, 0, 2); m_startAtLineSpinBox = new QSpinBox(page); m_startAtLineSpinBox->setObjectName("m_startAtLineSpinBox"); m_startAtLineSpinBox->setMinimum(1); m_startAtLineSpinBox->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); m_startAtLineSpinBox->setMinimumWidth( QFontMetrics(m_startAtLineSpinBox->font()).width("8888888")); glyr->addWidget(m_startAtLineSpinBox, 1, 3); m_startAtLineLabel = new QLabel(page); m_startAtLineLabel->setBuddy(m_startAtLineSpinBox); m_startAtLineLabel->setObjectName("m_startAtLineLabel"); m_startAtLineLabel->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred); m_startAtLineLabel->setAlignment(Qt::AlignLeft | Qt::AlignBottom); glyr->addWidget(m_startAtLineLabel, 0, 3); m_ignoreDuplicates = new QCheckBox(page); m_ignoreDuplicates->setObjectName("m_ignoreDuplicates"); m_ignoreDuplicates->setText(xi18n("Ignore duplicated delimiters")); glyr->addWidget(m_ignoreDuplicates, 2, 2, 1, 2); m_1stRowForFieldNames = new QCheckBox(page); m_1stRowForFieldNames->setObjectName("m_1stRowForFieldNames"); m_1stRowForFieldNames->setText(xi18n("First row contains column names")); glyr->addWidget(m_1stRowForFieldNames, 3, 2, 1, 2); QSpacerItem* spacer_2 = new QSpacerItem(0, 0, QSizePolicy::Minimum, QSizePolicy::Preferred); glyr->addItem(spacer_2, 0, 5, 4, 1); glyr->setColumnStretch(5, 2); m_tableView = new QTableView(m_optionsWidget); m_table = new KexiCSVImportDialogModel(m_tableView); m_table->setObjectName("m_table"); m_tableView->setModel(m_table); m_tableItemDelegate = new KexiCSVImportDialogItemDelegate(m_tableView); m_tableView->setItemDelegate(m_tableItemDelegate); lyr->addWidget(m_tableView); QSizePolicy spolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding); spolicy.setHorizontalStretch(1); spolicy.setVerticalStretch(1); m_tableView->setSizePolicy(spolicy); m_optionsPage = new KPageWidgetItem(m_optionsWidget, xi18n("Import Options")); addPage(m_optionsPage); } void KexiCSVImportDialog::createImportMethodPage() { m_saveMethodWidget = new QWidget(this); QGridLayout *l = new QGridLayout(m_saveMethodWidget); m_newTableButton = new KexiCommandLinkButton(xi18nc("@action:button", "New table"), xi18nc("CSV import: data will be appended to a new table", "Data will be appended to a new table"), m_saveMethodWidget); m_newTableButton->setArrowVisible(true); m_existentTableButton = new KexiCommandLinkButton(xi18nc("@action:button", "Existing table"), xi18nc("CSV import: data will be appended to existing table", "Data will be appended to existing table"), m_saveMethodWidget); m_existentTableButton->setArrowVisible(true); l->addWidget(m_newTableButton, 0, 0, 1, 1); l->addWidget(m_existentTableButton, 1, 0, 1, 1); QSpacerItem *hSpacer = new QSpacerItem(200, 20, QSizePolicy::Preferred, QSizePolicy::Minimum); QSpacerItem *vSpacer = new QSpacerItem(20, 200, QSizePolicy::Minimum, QSizePolicy::Expanding); l->addItem(hSpacer, 1, 1, 1, 1); l->addItem(vSpacer, 2, 0, 1, 1); m_saveMethodPage = new KPageWidgetItem(m_saveMethodWidget, xi18n("Choose Method of Saving Imported Data")); addPage(m_saveMethodPage); connect(m_newTableButton, SIGNAL(clicked()), this, SLOT(slotCommandLinkClicked())); connect(m_existentTableButton, SIGNAL(clicked()), this, SLOT(slotCommandLinkClicked())); } void KexiCSVImportDialog::createTableNamePage() { m_tableNameWidget = new QStackedWidget(this); m_tableNameWidget->setObjectName("m_tableNameWidget"); QWidget *page1=new QWidget(m_tableNameWidget); m_newTableWidget = new KexiNameWidget(QString(), page1); m_newTableWidget->addNameSubvalidator(new KDbObjectNameValidator( KexiMainWindowIface::global()->project()->dbConnection()->driver())); QVBoxLayout *l=new QVBoxLayout(page1); l->addWidget(m_newTableWidget); l->addStretch(1); m_tableNameWidget->addWidget(page1); QSplitter *splitter = new QSplitter(m_tableNameWidget); QWidget *tablesListParentWidget = new QWidget; QVBoxLayout *tablesListParentWidgetLayout = new QVBoxLayout(tablesListParentWidget); tablesListParentWidgetLayout->setMargin(0); QLabel *tablesListLabel = new QLabel(xi18nc("@label", "Select existing table:")); tablesListParentWidgetLayout->addWidget(tablesListLabel); KexiProjectNavigator::Features tablesListFeatures = KexiProjectNavigator::DefaultFeatures; tablesListFeatures &= (~KexiProjectNavigator::AllowSingleClickForOpeningItems); tablesListFeatures &= (~KexiProjectNavigator::ClearSelectionAfterAction); tablesListFeatures |= KexiProjectNavigator::Borders; m_tablesList = new KexiProjectNavigator(tablesListParentWidget, tablesListFeatures); tablesListParentWidgetLayout->addWidget(m_tablesList, 1); tablesListLabel->setBuddy(m_tablesList); QString errorString; m_tablesList->setProject(KexiMainWindowIface::global()->project(), "org.kexi-project.table", &errorString, false); connect (m_tablesList, SIGNAL(openOrActivateItem(KexiPart::Item*,Kexi::ViewMode)), this, SLOT(next())); connect (m_tablesList, SIGNAL(selectionChanged(KexiPart::Item*)), this, SLOT(slotShowSchema(KexiPart::Item*))); splitter->addWidget(tablesListParentWidget); QWidget *tableDetailsWidget = new QWidget; QFormLayout *formLayout = new QFormLayout(tableDetailsWidget); formLayout->setContentsMargins(KexiUtils::marginHint(), 0, 0, 0); formLayout->addRow(new QLabel(xi18nc("@label Preview of selected table", "Table preview:"))); formLayout->addRow(xi18nc("@label", "Name:"), m_tableNameLabel = new QLabel(tableDetailsWidget)); formLayout->addRow(xi18nc("@label", "Caption:"), m_tableCaptionLabel = new QLabel(tableDetailsWidget)); formLayout->addRow(xi18nc("@label", "Row count:"), m_recordCountLabel = new QLabel(tableDetailsWidget)); formLayout->addRow(xi18nc("@label", "Column count:"), m_colCountLabel = new QLabel(tableDetailsWidget)); formLayout->addItem(new QSpacerItem(1, KexiUtils::spacingHint())); m_fieldsListView = new QTreeView(tableDetailsWidget); m_fieldsListView->setItemsExpandable(false); m_fieldsListView->setRootIsDecorated(false); QSizePolicy fieldsListViewPolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); fieldsListViewPolicy.setVerticalStretch(1); m_fieldsListView->setSizePolicy(fieldsListViewPolicy); formLayout->addRow(new QLabel(xi18nc("@label", "Fields:"))); formLayout->addRow(m_fieldsListView); splitter->addWidget(tableDetailsWidget); splitter->setStretchFactor(splitter->indexOf(tableDetailsWidget), 1); m_tableNameWidget->addWidget(splitter); m_tableNamePage = new KPageWidgetItem(m_tableNameWidget, xi18nc("@label", "Choose Name of Destination Table")); addPage(m_tableNamePage); } void KexiCSVImportDialog::createImportPage() { m_importWidget = new QWidget(this); m_fromLabel = new KexiCSVInfoLabel(m_mode == File ? xi18n("From CSV file:") : xi18n("From Clipboard"), m_importWidget, m_mode == File); m_fromLabel->separator()->hide(); if (m_mode != File) { m_fromLabel->setIcon(koIconName("edit-paste")); } m_toLabel = new KexiCSVInfoLabel(xi18nc("@label Importing CSV data to table:", "To table:"), m_importWidget, true); KexiPart::Info *partInfo = Kexi::partManager().infoForPluginId("org.kexi-project.table"); m_toLabel->setIcon(partInfo->iconName()); m_importProgressLabel = new QLabel(m_importWidget); m_importingProgressBar = new QProgressBar(m_importWidget); QVBoxLayout *l = new QVBoxLayout(m_importWidget); l->addWidget(m_fromLabel); l->addWidget(m_toLabel); l->addSpacing(m_importProgressLabel->fontMetrics().height()); l->addWidget(m_importProgressLabel); l->addWidget(m_importingProgressBar); l->addStretch(1); m_importingProgressBar->hide(); m_importProgressLabel->hide(); m_importPage = new KPageWidgetItem(m_importWidget, xi18n("Importing...")); addPage(m_importPage); } void KexiCSVImportDialog::slotCommandLinkClicked() { if (m_tableNameWidget) { m_newTable = (sender() == m_newTableButton ? true : false); m_tableNameWidget->setCurrentIndex(sender() == m_newTableButton ? 0 : 1); next(); } } void KexiCSVImportDialog::initLater() { if (!openData()) return; m_columnsAdjusted = false; fillTable(); delete m_loadingProgressDlg; m_loadingProgressDlg = 0; if (m_dialogCanceled) { QTimer::singleShot(0, this, SLOT(reject())); return; } currentCellChanged(m_table->index(0,0), QModelIndex()); if (m_loadingProgressDlg) m_loadingProgressDlg->hide(); show(); m_tableView->setFocus(); } bool KexiCSVImportDialog::openData() { if (m_mode != File) //data already loaded, no encoding stuff needed return true; delete m_inputStream; m_inputStream = 0; if (m_file) { m_file->close(); delete m_file; } m_file = new QFile(m_fname); if (!m_file->open(QIODevice::ReadOnly)) { m_file->close(); delete m_file; m_file = 0; KMessageBox::sorry(this, xi18n("Cannot open input file %1.", QDir::toNativeSeparators(m_fname))); nextButton()->setEnabled(false); m_canceled = true; if (parentWidget()) parentWidget()->raise(); return false; } return true; } bool KexiCSVImportDialog::canceled() const { return m_canceled; } void KexiCSVImportDialog::fillTable() { KexiUtils::WaitCursor wc(true); repaint(); m_blockUserEvents = true; button(QDialogButtonBox::Cancel)->setEnabled(true); KexiUtils::WaitCursor wait; if (m_table->rowCount() > 0) //to accept editor m_tableView->setCurrentIndex(QModelIndex()); int row, column, maxColumn; QString field; m_table->clear(); d->clearDetectedTypes(); d->clearUniquenessTests(); m_primaryKeyColumn = -1; if (true != loadRows(field, row, column, maxColumn, true)) return; // file with only one line without EOL if (field.length() > 0) { setText(row - m_startline, column, field, true); ++row; field.clear(); } adjustRows(row - m_startline - (m_1stRowForFieldNames->isChecked() ? 1 : 0)); maxColumn = qMax(maxColumn, column); m_table->setColumnCount(maxColumn); for (column = 0; column < m_table->columnCount(); ++column) { updateColumn(column); if (!m_columnsAdjusted) m_tableView->resizeColumnToContents(column); } m_columnsAdjusted = true; if (m_primaryKeyColumn >= 0 && m_primaryKeyColumn < m_table->columnCount()) { if (KDbField::Integer != d->detectedType(m_primaryKeyColumn)) { setPrimaryKeyIcon(m_primaryKeyColumn, false); m_primaryKeyColumn = -1; } } m_tableView->setCurrentIndex(m_table->index(0, 0)); currentCellChanged(m_table->index(0, 0), QModelIndex()); setPrimaryKeyIcon(m_primaryKeyColumn, true); const int count = qMax(0, m_table->rowCount() - 1 + m_startline); m_allRowsLoadedInPreview = count < m_maximumRowsForPreview && !m_stoppedAt_MAX_BYTES_TO_PREVIEW; if (count > 1) { if (m_allRowsLoadedInPreview) { m_startAtLineSpinBox->setMaximum(count); m_startAtLineSpinBox->setValue(m_startline + 1); } m_startAtLineSpinBox->setEnabled(true); m_startAtLineLabel->setText( m_allRowsLoadedInPreview ? xi18n("Start at line (1-%1):", count) : xi18n("Start at line:") //we do not know what's real count ); m_startAtLineLabel->setEnabled(true); } else { // no data m_startAtLineSpinBox->setMaximum(1); m_startAtLineSpinBox->setValue(1); m_startAtLineSpinBox->setEnabled(false); m_startAtLineLabel->setText(xi18n("Start at line:")); m_startAtLineLabel->setEnabled(false); } updateRowCountInfo(); m_blockUserEvents = false; repaint(); } QString KexiCSVImportDialog::detectDelimiterByLookingAtFirstBytesOfFile(QTextStream *inputStream) { // try to detect delimiter // \t has priority, then ; then , const qint64 origOffset = inputStream->pos(); QChar c, prevChar = 0; int detectedDelimiter = 0; bool insideQuote = false; //characters by priority const int CH_TAB_AFTER_QUOTE = 500; const int CH_SEMICOLON_AFTER_QUOTE = 499; const int CH_COMMA_AFTER_QUOTE = 498; const int CH_TAB = 200; // \t const int CH_SEMICOLON = 199; // ; const int CH_COMMA = 198; // , QList tabsPerLine, semicolonsPerLine, commasPerLine; int tabs = 0, semicolons = 0, commas = 0; int line = 0; bool wasChar13 = false; // true if previous x was '\r' for (int i = 0; !inputStream->atEnd() && i < MAX_CHARS_TO_SCAN_WHILE_DETECTING_DELIMITER; i++) { (*m_inputStream) >> c; // read one char if (prevChar == '"') { if (c != '"') //real quote (not double "") insideQuote = !insideQuote; } if (insideQuote) { prevChar = c; continue; } if (c == ' ') continue; if (wasChar13 && c == '\n') { wasChar13 = false; continue; // previous x was '\r', eat '\n' } wasChar13 = c == '\r'; if (c == '\n' || c == '\r') {//end of line //remember # of tabs/semicolons/commas in this line tabsPerLine += tabs; tabs = 0; semicolonsPerLine += semicolons; semicolons = 0; commasPerLine += commas; commas = 0; line++; } else if (c == '\t') { tabs++; detectedDelimiter = qMax(prevChar == '"' ? CH_TAB_AFTER_QUOTE : CH_TAB, detectedDelimiter); } else if (c == ';') { semicolons++; detectedDelimiter = qMax(prevChar == '"' ? CH_SEMICOLON_AFTER_QUOTE : CH_SEMICOLON, detectedDelimiter); } else if (c == ',') { commas++; detectedDelimiter = qMax(prevChar == '"' ? CH_COMMA_AFTER_QUOTE : CH_COMMA, detectedDelimiter); } prevChar = c; } inputStream->seek(origOffset); //restore orig. offset //now, try to find a delimiter character that exists the same number of times in all the checked lines //this detection method has priority over others QList::ConstIterator it; if (tabsPerLine.count() > 1) { tabs = tabsPerLine.isEmpty() ? 0 : tabsPerLine.first(); for (it = tabsPerLine.constBegin(); it != tabsPerLine.constEnd(); ++it) { if (tabs != *it) break; } if (tabs > 0 && it == tabsPerLine.constEnd()) return "\t"; } if (semicolonsPerLine.count() > 1) { semicolons = semicolonsPerLine.isEmpty() ? 0 : semicolonsPerLine.first(); for (it = semicolonsPerLine.constBegin(); it != semicolonsPerLine.constEnd(); ++it) { if (semicolons != *it) break; } if (semicolons > 0 && it == semicolonsPerLine.constEnd()) return ";"; } if (commasPerLine.count() > 1) { commas = commasPerLine.first(); for (it = commasPerLine.constBegin(); it != commasPerLine.constEnd(); ++it) { if (commas != *it) break; } if (commas > 0 && it == commasPerLine.constEnd()) return ","; } //now return the winning character by looking at CH_* symbol if (detectedDelimiter == CH_TAB_AFTER_QUOTE || detectedDelimiter == CH_TAB) return "\t"; if (detectedDelimiter == CH_SEMICOLON_AFTER_QUOTE || detectedDelimiter == CH_SEMICOLON) return ";"; if (detectedDelimiter == CH_COMMA_AFTER_QUOTE || detectedDelimiter == CH_COMMA) return ","; return KEXICSV_DEFAULT_FILE_DELIMITER; //<-- default } tristate KexiCSVImportDialog::loadRows(QString &field, int &row, int &column, int &maxColumn, bool inGUI) { enum { S_START, S_QUOTED_FIELD, S_MAYBE_END_OF_QUOTED_FIELD, S_END_OF_QUOTED_FIELD, S_MAYBE_NORMAL_FIELD, S_NORMAL_FIELD, S_COMMENT } state = S_START; field.clear(); const bool ignoreDups = m_ignoreDuplicates->isChecked(); bool lastCharDelimiter = false; bool nextRow = false; row = column = 1; m_prevColumnForSetText = 0; maxColumn = 0; QChar x; const bool hadInputStream = m_inputStream != 0; delete m_inputStream; if (m_mode == Clipboard) { m_inputStream = new QTextStream(&m_clipboardData, QIODevice::ReadOnly); if (!hadInputStream) m_delimiterWidget->setDelimiter(KEXICSV_DEFAULT_CLIPBOARD_DELIMITER); } else { m_file->seek(0); //always seek at 0 because loadRows() is called many times m_inputStream = new QTextStream(m_file); QTextCodec *codec = KCharsets::charsets()->codecForName(m_options.encoding); if (codec) { m_inputStream->setCodec(codec); //QTextCodec::codecForName("CP1250")); } if (m_detectDelimiter) { const QString delimiter(detectDelimiterByLookingAtFirstBytesOfFile(m_inputStream)); if (m_delimiterWidget->delimiter() != delimiter) m_delimiterWidget->setDelimiter(delimiter); } } const QChar delimiter(m_delimiterWidget->delimiter()[0]); const QChar commentSymbol(m_commentWidget->commentSymbol()[0]); m_stoppedAt_MAX_BYTES_TO_PREVIEW = false; if (m_importingProgressBar) { m_elapsedTimer.start(); m_elapsedMs = m_elapsedTimer.elapsed(); } int offset = 0; bool wasChar13 = false; // true if previous x was '\r' for (;; ++offset) { if (m_importingProgressBar && (offset % 0x100) == 0 && (m_elapsedMs + PROGRESS_STEP_MS) < m_elapsedTimer.elapsed()) { //update progr. bar dlg on final exporting m_elapsedMs = m_elapsedTimer.elapsed(); m_importingProgressBar->setValue(offset); qApp->processEvents(); if (m_importCanceled) { return ::cancelled; } } if (m_inputStream->atEnd()) { if (x != '\n' && x != '\r') { x = '\n'; // simulate missing \n at end wasChar13 = false; } else { break; // finish! } } else { (*m_inputStream) >> x; // read one char } if (wasChar13 && x == '\n') { wasChar13 = false; continue; // previous x was '\r', eat '\n' } wasChar13 = x == '\r'; if (offset == 0 && x.unicode() == 0xfeff) { // Ignore BOM, the "Byte Order Mark" // (http://en.wikipedia.org/wiki/Byte_Order_Mark, // http://www.unicode.org/charts/PDF/UFFF0.pdf) // Probably fixed in Qt4. continue; } switch (state) { case S_START : if (x == m_textquote) { state = S_QUOTED_FIELD; } else if (x == delimiter) { field.clear(); if ((ignoreDups == false) || (lastCharDelimiter == false)) ++column; lastCharDelimiter = true; } else if (x == '\n' || x == '\r' || (x == commentSymbol && m_parseComments)) { if (!inGUI) { //fill remaining empty fields (database wants them explicitly) for (int additionalColumn = column; additionalColumn <= maxColumn; additionalColumn++) { setText(row - m_startline, additionalColumn, QString(), inGUI); } } nextRow = true; if (ignoreDups && lastCharDelimiter) { // we're ignoring repeated delimiters so remove any extra trailing delimiters --column; } maxColumn = qMax(maxColumn, column); column = 1; m_prevColumnForSetText = 0; if (x == commentSymbol && m_parseComments) { state = S_COMMENT; maxColumn -= 1; break; } } else { field += x; state = S_MAYBE_NORMAL_FIELD; } break; case S_QUOTED_FIELD : if (x == m_textquote) { state = S_MAYBE_END_OF_QUOTED_FIELD; } /*allow \n inside quoted fields else if (x == '\n') { setText(row - m_startline, column, field, inGUI); field = ""; if (x == '\n') { nextRow = true; maxColumn = qMax( maxColumn, column ); column = 1; } else { if ((ignoreDups == false) || (lastCharDelimiter == false)) ++column; lastCharDelimiter = true; } state = S_START; }*/ else { field += x; } break; case S_MAYBE_END_OF_QUOTED_FIELD : if (x == m_textquote) { field += x; //no, this was just escaped quote character state = S_QUOTED_FIELD; } else if (x == delimiter || x == '\n' || x == '\r' || (x == commentSymbol && m_parseComments)) { setText(row - m_startline, column, field, inGUI); field.clear(); if (x == '\n' || x == '\r' || (x == commentSymbol && m_parseComments)) { nextRow = true; maxColumn = qMax(maxColumn, column); column = 1; m_prevColumnForSetText = 0; if (x == commentSymbol && m_parseComments) { state = S_COMMENT; break; } } else { if ((ignoreDups == false) || (lastCharDelimiter == false)) ++column; lastCharDelimiter = true; } state = S_START; } else { state = S_END_OF_QUOTED_FIELD; } break; case S_END_OF_QUOTED_FIELD : if (x == delimiter || x == '\n' || x == '\r' || (x == commentSymbol && m_parseComments)) { setText(row - m_startline, column, field, inGUI); field.clear(); if (x == '\n' || x == '\r' || (x == commentSymbol && m_parseComments)) { nextRow = true; maxColumn = qMax(maxColumn, column); column = 1; m_prevColumnForSetText = 0; if (x == commentSymbol && m_parseComments) { state = S_COMMENT; break; } } else { if ((ignoreDups == false) || (lastCharDelimiter == false)) ++column; lastCharDelimiter = true; } state = S_START; } else { state = S_END_OF_QUOTED_FIELD; } break; case S_COMMENT : if (x == '\n' || x == '\r') { state = S_START; } if (lastCharDelimiter) { lastCharDelimiter = false; } break; case S_MAYBE_NORMAL_FIELD : if (x == m_textquote) { field.clear(); state = S_QUOTED_FIELD; break; } case S_NORMAL_FIELD : if (x == delimiter || x == '\n' || x == '\r' || (x == commentSymbol && m_parseComments)) { setText(row - m_startline, column, field, inGUI); field.clear(); if (x == '\n' || x == '\r' || (x == commentSymbol && m_parseComments)) { nextRow = true; maxColumn = qMax(maxColumn, column); column = 1; m_prevColumnForSetText = 0; if (x == commentSymbol && m_parseComments) { state = S_COMMENT; break; } } else { if ((ignoreDups == false) || (lastCharDelimiter == false)) ++column; lastCharDelimiter = true; } state = S_START; } else { field += x; } } if (x != delimiter) lastCharDelimiter = false; if (nextRow) { if (!inGUI && !shouldSaveRow(row - m_startline, m_1stRowForFieldNames->isChecked())) { // do not save to the database 1st row if it contains column names m_valuesToInsert.clear(); } else if (!saveRow(inGUI)) return false; ++row; } if (m_firstFillTableCall && row == 2 && !m_1stRowForFieldNames->isChecked() && m_table->firstRowForFieldNames()) { m_table->clear(); m_firstFillTableCall = false; //this trick is allowed only once, on startup m_1stRowForFieldNames->setChecked(true); //this will reload table m_blockUserEvents = false; repaint(); return false; } if (!m_importingProgressBar && row % 20 == 0) { qApp->processEvents(); //only for GUI mode: if (!m_firstFillTableCall && m_loadingProgressDlg && m_loadingProgressDlg->wasCanceled()) { delete m_loadingProgressDlg; m_loadingProgressDlg = 0; m_dialogCanceled = true; reject(); return false; } } if (!m_firstFillTableCall && m_loadingProgressDlg) { m_loadingProgressDlg->setValue(qMin(m_maximumRowsForPreview, row)); } if (inGUI && row > (m_maximumRowsForPreview + (m_table->firstRowForFieldNames() ? 1 : 0))) { qDebug() << "loading stopped at row #" << m_maximumRowsForPreview; break; } if (nextRow) { nextRow = false; //additional speedup: stop processing now if too many bytes were loaded for preview qDebug() << offset; if (inGUI && offset >= m_maximumBytesForPreview && row >= 2) { m_stoppedAt_MAX_BYTES_TO_PREVIEW = true; return true; } } } return true; } void KexiCSVImportDialog::updateColumn(int col) { KDbField::Type detectedType = d->detectedType(col); if (detectedType == KDbField::InvalidType) { d->setDetectedType(col, KDbField::Text); //entirely empty column detectedType = KDbField::Text; } m_table->setHeaderData(col, Qt::Horizontal, QString(xi18n("Column %1", col + 1) + " \n(" + kexiCSVImportStatic->typeNames[detectedType].toLower() + ") ")); m_tableView->horizontalHeader()->adjustSize(); if (m_primaryKeyColumn == -1 && isPrimaryKeyAllowed(col)) { m_primaryKeyColumn = col; } } bool KexiCSVImportDialog::isPrimaryKeyAllowed(int col) { QList *list = d->uniquenessTest(col); if (m_primaryKeyColumn != -1 || !list || list->isEmpty()) { return false; } bool result = false; int expectedRowCount = m_table->rowCount(); if (m_table->firstRowForFieldNames()) { expectedRowCount--; } if (list->count() == expectedRowCount) { qSort(*list); QList::ConstIterator it = list->constBegin(); int prevValue = *it; ++it; for (; it != list->constEnd() && prevValue != (*it); ++it) { prevValue = (*it); } result = it == list->constEnd(); // no duplicates } list->clear(); // not needed now: conserve memory return result; } void KexiCSVImportDialog::detectTypeAndUniqueness(int row, int col, const QString& text) { int intValue; KDbField::Type type = d->detectedType(col); if (row == 1 || type != KDbField::Text) { bool found = false; if (text.isEmpty() && type == KDbField::InvalidType) found = true; //real type should be found later //detect type because it's 1st row or all prev. rows were not text //-FP number? (trying before "number" type is a must) if (!found && (row == 1 || type == KDbField::Integer || type == KDbField::Double || type == KDbField::InvalidType)) { - bool ok = text.isEmpty() || m_fpNumberRegExp1.exactMatch(text) || m_fpNumberRegExp2.exactMatch(text); + bool ok = text.isEmpty() || m_fpNumberRegExp1.match(text).hasMatch() || m_fpNumberRegExp2.match(text).hasMatch(); if (ok && (row == 1 || type == KDbField::InvalidType)) { d->setDetectedType(col, KDbField::Double); found = true; //yes } } //-number? if (!found && (row == 1 || type == KDbField::Integer || type == KDbField::InvalidType)) { bool ok = text.isEmpty();//empty values allowed if (!ok) intValue = text.toInt(&ok); if (ok && (row == 1 || type == KDbField::InvalidType)) { d->setDetectedType(col, KDbField::Integer); found = true; //yes } } //-date? if (!found && (row == 1 || type == KDbField::Date || type == KDbField::InvalidType)) { if ((row == 1 || type == KDbField::InvalidType) - && (text.isEmpty() || m_dateRegExp.exactMatch(text))) { + && (text.isEmpty() || m_dateRegExp.match(text).hasMatch())) { d->setDetectedType(col, KDbField::Date); found = true; //yes } } //-time? if (!found && (row == 1 || type == KDbField::Time || type == KDbField::InvalidType)) { if ((row == 1 || type == KDbField::InvalidType) - && (text.isEmpty() || m_timeRegExp1.exactMatch(text) || m_timeRegExp2.exactMatch(text))) + && (text.isEmpty() || m_timeRegExp1.match(text).hasMatch() || m_timeRegExp2.match(text).hasMatch())) { d->setDetectedType(col, KDbField::Time); found = true; //yes } } //-date/time? if (!found && (row == 1 || type == KDbField::Time || type == KDbField::InvalidType)) { if (row == 1 || type == KDbField::InvalidType) { bool detected = text.isEmpty(); if (!detected) { const QStringList dateTimeList(text.split(' ')); bool ok = dateTimeList.count() >= 2; //! @todo also support ISODateTime's "T" separator? //! @todo also support timezones? if (ok) { //try all combinations QString datePart(dateTimeList[0].trimmed()); QString timePart(dateTimeList[1].trimmed()); - ok = m_dateRegExp.exactMatch(datePart) - && (m_timeRegExp1.exactMatch(timePart) || m_timeRegExp2.exactMatch(timePart)); + ok = m_dateRegExp.match(datePart).hasMatch() + && (m_timeRegExp1.match(timePart).hasMatch() || m_timeRegExp2.match(timePart).hasMatch()); } detected = ok; } if (detected) { d->setDetectedType(col, KDbField::DateTime); found = true; //yes } } } if (!found && type == KDbField::InvalidType && !text.isEmpty()) { //eventually, a non-emptytext after a while d->setDetectedType(col, KDbField::Text); found = true; //yes } //default: text type (already set) } type = d->detectedType(col); //qDebug() << type; if (type == KDbField::Integer) { // check uniqueness for this value QList *list = d->uniquenessTest(col); if (text.isEmpty()) { if (list) { list->clear(); // empty value cannot be in PK } } else { if (!list) { list = new QList(); d->setUniquenessTest(col, list); } list->append(intValue); } } } QDate KexiCSVImportDialog::buildDate(int y, int m, int d) const { if (y < 100) { if ((1900 + y) >= m_minimumYearFor100YearSlidingWindow) return QDate(1900 + y, m, d); else return QDate(2000 + y, m, d); } return QDate(y, m, d); } bool KexiCSVImportDialog::parseDate(const QString& text, QDate& date) { - if (!m_dateRegExp.exactMatch(text)) + QRegularExpressionMatch match = m_dateRegExp.match(text); + if (!match.hasMatch()) return false; //dddd - dd - dddd //1 2 3 4 5 <- pos - const int d1 = m_dateRegExp.cap(1).toInt(), d3 = m_dateRegExp.cap(3).toInt(), d5 = m_dateRegExp.cap(5).toInt(); + const int d1 = match.captured(1).toInt(), d3 = match.captured(3).toInt(), d5 = match.captured(5).toInt(); switch (m_options.dateFormat) { case KexiCSVImportOptions::DMY: date = buildDate(d5, d3, d1); break; case KexiCSVImportOptions::YMD: date = buildDate(d1, d3, d5); break; case KexiCSVImportOptions::MDY: date = buildDate(d5, d1, d3); break; case KexiCSVImportOptions::AutoDateFormat: - if (m_dateRegExp.cap(2) == "/") { //probably separator for american format mm/dd/yyyy + if (match.captured(2) == "/") { //probably separator for american format mm/dd/yyyy date = buildDate(d5, d1, d3); } else { if (d5 > 31) //d5 == year date = buildDate(d5, d3, d1); else //d1 == year date = buildDate(d1, d3, d5); } break; default:; } return date.isValid(); } bool KexiCSVImportDialog::parseTime(const QString& text, QTime& time) { time = QTime::fromString(text, Qt::ISODate); //same as m_timeRegExp1 if (time.isValid()) return true; - if (m_timeRegExp2.exactMatch(text)) { //hh:mm:ss - time = QTime(m_timeRegExp2.cap(1).toInt(), - m_timeRegExp2.cap(3).toInt(), m_timeRegExp2.cap(5).toInt()); + + QRegularExpressionMatch match = m_timeRegExp2.match(text); + if (match.hasMatch()) { //hh:mm:ss + time = QTime(match.captured(1).toInt(), + match.captured(3).toInt(), match.captured(5).toInt()); return true; } return false; } void KexiCSVImportDialog::setText(int row, int col, const QString& text, bool inGUI) { if (!inGUI) { if (!shouldSaveRow(row, m_1stRowForFieldNames->isChecked())) return; // do not care about this value if it contains column names (these were already used) //save text directly to database buffer if (m_prevColumnForSetText == 0) { //1st call m_valuesToInsert.clear(); if (m_implicitPrimaryKeyAdded) { m_valuesToInsert << QVariant(); //id will be autogenerated here } } if ((m_prevColumnForSetText + 1) < col) { //skipped one or more columns //before this: save NULLs first for (int i = m_prevColumnForSetText + 1; i < col; i++) { if (m_options.nullsImportedAsEmptyTextChecked && KDbField::isTextType(d->detectedType(i-1))) { m_valuesToInsert << QString(""); } else { m_valuesToInsert << QVariant(); } } } m_prevColumnForSetText = col; const KDbField::Type detectedType = d->detectedType(col-1); if (detectedType == KDbField::Integer) { m_valuesToInsert << (text.isEmpty() ? QVariant() : text.toInt()); //! @todo what about time and float/double types and different integer subtypes? } else if (detectedType == KDbField::Double) { //replace ',' with '.' QByteArray t(text.toLatin1()); const int textLen = t.length(); for (int i = 0; i < textLen; i++) { if (t[i] == ',') { t[i] = '.'; break; } } m_valuesToInsert << (t.isEmpty() ? QVariant() : t.toDouble()); } else if (detectedType == KDbField::Boolean) { const QString t(text.trimmed().toLower()); if (t.isEmpty()) m_valuesToInsert << QVariant(); else if (t == "0" || t == m_stringNo || t == m_stringI18nNo || t == m_stringFalse || t == m_stringI18nFalse) m_valuesToInsert << QVariant(false); else m_valuesToInsert << QVariant(true); //anything nonempty } else if (detectedType == KDbField::Date) { QDate date; if (parseDate(text, date)) m_valuesToInsert << date; else m_valuesToInsert << QVariant(); } else if (detectedType == KDbField::Time) { QTime time; if (parseTime(text, time)) m_valuesToInsert << time; else m_valuesToInsert << QVariant(); } else if (detectedType == KDbField::DateTime) { QStringList dateTimeList(text.split(' ')); if (dateTimeList.count() < 2) dateTimeList = text.split('T'); //also support ISODateTime's "T" separator //! @todo also support timezones? if (dateTimeList.count() >= 2) { //try all combinations QString datePart(dateTimeList[0].trimmed()); QDate date; if (parseDate(datePart, date)) { QString timePart(dateTimeList[1].trimmed()); QTime time; if (parseTime(timePart, time)) m_valuesToInsert << QDateTime(date, time); else m_valuesToInsert << QVariant(); } else m_valuesToInsert << QVariant(); } else m_valuesToInsert << QVariant(); } else { // Text type and the rest if (m_options.nullsImportedAsEmptyTextChecked && text.isNull()) { //default value is empty string not null - otherwise querying data without knowing SQL is very confusing m_valuesToInsert << QString(""); } else { m_valuesToInsert <columnCount() < col) { m_table->setColumnCount(col); } if (!m_1stRowForFieldNames->isChecked()) { if ((row + m_startline) == 1) {//this row is for column name if (m_table->firstRowForFieldNames() && !m_1stRowForFieldNames->isChecked()) { QString f(text.simplified()); if (f.isEmpty() || !f[0].isLetter()) { m_table->setFirstRowForFieldNames(false); } } } row++; //1st row was for column names } else { if ((row + m_startline) == 1) {//this is for column name m_table->setRowCount(1); QString colName(text.simplified()); if (!colName.isEmpty()) { if (colName.left(1) >= "0" && colName.left(1) <= "9") colName.prepend(xi18n("Column") + " "); m_table->setData(m_table->index(0, col - 1), colName); } return; } } if (row < 2) // skipped by the user return; if (m_table->rowCount() < row) { m_table->setRowCount(row + 100); /* We add more rows at a time to limit recalculations */ m_adjustRows = true; } m_table->setData(m_table->index(row-1 ,col-1),m_options.trimmedInTextValuesChecked ? text.trimmed() : text); detectTypeAndUniqueness(row - 1, col - 1, text); } bool KexiCSVImportDialog::saveRow(bool inGUI) { if (inGUI) { //nothing to do return true; } bool res = m_importingStatement.execute(m_valuesToInsert); //! @todo move if (!res) { const QStringList msgList = KexiUtils::convertTypesUsingMethod(m_valuesToInsert); const KMessageBox::ButtonCode msgRes = KMessageBox::warningContinueCancelList(this, xi18nc("@info", "An error occurred during insert record."), QStringList(msgList.join(";")), QString(), KStandardGuiItem::cont(), KStandardGuiItem::cancel(), "SkipImportErrors" ); res = msgRes == KMessageBox::Continue; } m_valuesToInsert.clear(); return res; } void KexiCSVImportDialog::adjustRows(int iRows) { if (m_adjustRows) { m_table->setRowCount(iRows); m_adjustRows = false; for (int i = 0; i < iRows; i++) m_tableView->resizeRowToContents(i); } } void KexiCSVImportDialog::formatChanged(int index) { if (index < 0 || index >= kexiCSVImportStatic->types.size()) return; KDbField::Type type = kexiCSVImportStatic->types[index]; d->setDetectedType(m_tableView->currentIndex().column(), type); m_primaryKeyField->setEnabled(KDbField::Integer == type); m_primaryKeyField->setChecked(m_primaryKeyColumn == m_tableView->currentIndex().column() && m_primaryKeyField->isEnabled()); updateColumn(m_tableView->currentIndex().column()); } void KexiCSVImportDialog::delimiterChanged(const QString& delimiter) { Q_UNUSED(delimiter); m_columnsAdjusted = false; m_detectDelimiter = false; //selected by hand: do not detect in the future //delayed, otherwise combobox won't be repainted fillTableLater(); } void KexiCSVImportDialog::commentSymbolChanged(const QString& commentSymbol) { QString noneString = QString(xi18n("None")); if (commentSymbol.compare(noneString) == 0) { m_parseComments = false; } else { m_parseComments = true; } m_columnsAdjusted = false; m_detectDelimiter = false; //selected by hand: do not detect in the future //delayed, otherwise combobox won't be repainted fillTableLater(); } void KexiCSVImportDialog::textquoteSelected(int) { const QString tq(m_comboQuote->textQuote()); if (tq.isEmpty()) m_textquote = 0; else m_textquote = tq[0]; qDebug() << m_textquote; //delayed, otherwise combobox won't be repainted fillTableLater(); } void KexiCSVImportDialog::fillTableLater() { m_table->setColumnCount(0); QTimer::singleShot(10, this, SLOT(fillTable())); } void KexiCSVImportDialog::startlineSelected(int startline) { if (m_startline == (startline - 1)) return; m_startline = startline - 1; m_adjustRows = true; m_columnsAdjusted = false; fillTable(); m_tableView->setFocus(); } void KexiCSVImportDialog::currentCellChanged(const QModelIndex &cur, const QModelIndex &prev) { if (prev.column() == cur.column() || !cur.isValid()) return; const KDbField::Type type = d->detectedType(cur.column()); m_formatCombo->setCurrentIndex(kexiCSVImportStatic->indicesForTypes.value(type, -1)); m_formatLabel->setText(xi18n("Format for column %1:", cur.column() + 1)); m_primaryKeyField->setEnabled(KDbField::Integer == type); m_primaryKeyField->blockSignals(true); //block to disable executing slotPrimaryKeyFieldToggled() m_primaryKeyField->setChecked(m_primaryKeyColumn == cur.column()); m_primaryKeyField->blockSignals(false); } //! Used in emergency by accept() void KexiCSVImportDialog::dropDestinationTable(KexiProject* project, KexiPart::Item* &partItemForSavedTable) { m_importingProgressBar->hide(); project->deleteUnstoredItem(partItemForSavedTable); partItemForSavedTable = 0; m_conn->dropTable(m_destinationTableSchema); /*alsoRemoveSchema*/ m_destinationTableSchema = 0; m_conn = 0; } //! Used in emergency by accept() void KexiCSVImportDialog::raiseErrorInAccept(KexiProject* project, KexiPart::Item* &partItemForSavedTable) { finishButton()->setEnabled(true); KGuiItem::assign(finishButton(), KGuiItem(xi18nc("@action:button Import CSV", "&Import..."), _IMPORT_ICON)); project->deleteUnstoredItem(partItemForSavedTable); partItemForSavedTable = 0; delete m_destinationTableSchema; m_destinationTableSchema = 0; m_conn = 0; backButton()->setEnabled(true); m_importInProgress = false; m_importingProgressBar->hide(); } void KexiCSVImportDialog::accept() { if (d->imported) { parentWidget()->raise(); bool openingCanceled; KexiWindow *win = KexiMainWindowIface::global()->openedWindowFor(m_partItemForSavedTable); if (win) { KexiMainWindowIface::global()->closeObject(m_partItemForSavedTable); } KexiMainWindowIface::global()->openObject(m_partItemForSavedTable, Kexi::DataViewMode, &openingCanceled); KAssistantDialog::accept(); } else { import(); } } void KexiCSVImportDialog::import() { //! @todo MOVE MOST OF THIS TO CORE/ (KexiProject?) after KexiWindow code is moved to non-gui place KMessageBox::enableMessage("SkipImportErrors"); KexiGUIMessageHandler msg; //! @todo make it better integrated with main window KexiProject *project = KexiMainWindowIface::global()->project(); if (!project) { msg.showErrorMessage(KDbMessageHandler::Error, xi18n("No project available.")); return; } m_conn = project->dbConnection(); if (!m_conn) { msg.showErrorMessage(KDbMessageHandler::Error, xi18n("No database connection available.")); return; } if (m_newTable) { m_destinationTableSchema = new KDbTableSchema(m_partItemForSavedTable->name()); m_destinationTableSchema->setCaption(m_partItemForSavedTable->caption()); m_destinationTableSchema->setDescription(m_partItemForSavedTable->description()); const int numCols(m_table->columnCount()); m_implicitPrimaryKeyAdded = false; //add PK if user wanted it int msgboxResult; if ( m_primaryKeyColumn == -1 && KMessageBox::No != (msgboxResult = KMessageBox::questionYesNoCancel(this, xi18nc("@info", "No primary key (autonumber) has been defined." "Should it be automatically defined on import (recommended)?" "An imported table without a primary key may not be " "editable (depending on database type)."), QString(), KGuiItem(xi18nc("@action:button Add Database Primary Key to a Table", "&Add Primary Key"), KexiIconName("database-key")), KGuiItem(xi18nc("@action:button Do Not Add Database Primary Key to a Table", "Do &Not Add"), KStandardGuiItem::no().icon())))) { if (msgboxResult == KMessageBox::Cancel) { raiseErrorInAccept(project, m_partItemForSavedTable); return; //cancel accepting } //add implicit PK field //! @todo make this field hidden (what about e.g. pgsql?) m_implicitPrimaryKeyAdded = true; QString fieldName("id"); QString fieldCaption("Id"); QSet colnames; for (int col = 0; col < numCols; col++) colnames.insert(m_table->data(m_table->index(0, col)).toString().toLower().simplified()); if (colnames.contains(fieldName)) { int num = 1; while (colnames.contains(fieldName + QString::number(num))) num++; fieldName += QString::number(num); fieldCaption += QString::number(num); } KDbField *field = new KDbField( fieldName, KDbField::Integer, KDbField::NoConstraints, KDbField::NoOptions, 0, 0, //int length=0, int precision=0, QVariant(), //QVariant defaultValue=QVariant(), fieldCaption ); //no description and width for now field->setPrimaryKey(true); field->setAutoIncrement(true); if (!m_destinationTableSchema->addField(field)) { msg.showErrorMessage(KDbMessageHandler::Error, xi18n("Cannot add column.")); delete field; delete m_destinationTableSchema; m_destinationTableSchema = 0; return; } } for (int col = 0; col < numCols; col++) { QString fieldCaption(m_table->data(m_table->index(0, col)).toString().simplified()); QString fieldName; if (fieldCaption.isEmpty()) { int i = 0; do { fieldCaption = xi18nc("@title:column Column 1, Column 2, etc.", "Column %1", i + 1); fieldName = KDb::stringToIdentifier(fieldCaption); if (!m_destinationTableSchema->field(fieldName)) { break; } i++; } while (true); } else { fieldName = KDb::stringToIdentifier(fieldCaption); if (m_destinationTableSchema->field(fieldName)) { QString fixedFieldName; int i = 2; //"apple 2, apple 3, etc. if there're many "apple" names do { fixedFieldName = fieldName + "_" + QString::number(i); if (!m_destinationTableSchema->field(fixedFieldName)) break; i++; } while (true); fieldName = fixedFieldName; fieldCaption += (" " + QString::number(i)); } } KDbField::Type detectedType = d->detectedType(col); //! @todo what about time and float/double types and different integer subtypes? //! @todo what about long text? if (detectedType == KDbField::InvalidType) { detectedType = KDbField::Text; } KDbField *field = new KDbField( fieldName, detectedType, KDbField::NoConstraints, KDbField::NoOptions, 0, 0, //int length=0, int precision=0, QVariant(), //QVariant defaultValue=QVariant(), fieldCaption ); //no description and width for now if ((int)col == m_primaryKeyColumn) { field->setPrimaryKey(true); field->setAutoIncrement(true); } if (!m_destinationTableSchema->addField(field)) { msg.showErrorMessage(KDbMessageHandler::Error, xi18n("Cannot add column.")); delete field; delete m_destinationTableSchema; m_destinationTableSchema = 0; return; } } } else { m_implicitPrimaryKeyAdded = false; m_destinationTableSchema = m_conn->tableSchema(m_partItemForSavedTable->name()); int firstColumn = 0; if (m_destinationTableSchema->field(0)->isPrimaryKey() && m_primaryKeyColumn == -1) { m_implicitPrimaryKeyAdded = true; firstColumn = 1; } if (m_destinationTableSchema->fields()->size() - firstColumn < m_table->columnCount()) { KMessageBox::error(this, xi18n("Field count does not match." "Please choose another table.")); return; } } m_importInProgress = true; backButton()->setEnabled(false); finishButton()->setEnabled(false); KexiPart::Part *part = Kexi::partManager().partForPluginId("org.kexi-project.table"); if (!part) { msg.showErrorMessage(Kexi::partManager().result()); return; } KDbTransaction transaction = m_conn->beginTransaction(); if (transaction.isNull()) { msg.showErrorMessage(m_conn->result()); raiseErrorInAccept(project, m_partItemForSavedTable); return; } KDbTransactionGuard tg(transaction); //-create physical table if (m_newTable && !m_conn->createTable(m_destinationTableSchema, false /*allowOverwrite*/)) { msg.showErrorMessage(m_conn->result()); raiseErrorInAccept(project, m_partItemForSavedTable); return; } m_importingStatement = m_conn->prepareStatement( KDbPreparedStatement::InsertStatement, m_destinationTableSchema); if (!m_importingStatement.isValid()) { msg.showErrorMessage(m_conn->result()); raiseErrorInAccept(project, m_partItemForSavedTable); return; } if (m_file) { m_importProgressLabel->setText(xi18n("Importing data...")); m_importingProgressBar->setMaximum(QFileInfo(*m_file).size() - 1); m_importingProgressBar->show(); m_importProgressLabel->show(); } int row, column, maxColumn; QString field; // main job tristate res = loadRows(field, row, column, maxColumn, false /*!gui*/); if (true != res) { //importing canceled or failed if (!res) { //do not display err msg when res == cancelled m_importProgressLabel->setText(xi18n("Import has been canceled.")); } else if (~res) { m_importProgressLabel->setText(xi18n("Error occurred during import.")); } raiseErrorInAccept(project, m_partItemForSavedTable); return; } // file with only one line without '\n' if (field.length() > 0) { setText(row - m_startline, column, field, false /*!gui*/); //fill remaining empty fields (database wants them explicitly) for (int additionalColumn = column; additionalColumn <= maxColumn; additionalColumn++) { setText(row - m_startline, additionalColumn, QString(), false /*!gui*/); } if (!saveRow(false /*!gui*/)) { msg.showErrorMessage(m_conn->result()); raiseErrorInAccept(project, m_partItemForSavedTable); return; } ++row; field.clear(); } if (!tg.commit()) { msg.showErrorMessage(m_conn->result()); raiseErrorInAccept(project, m_partItemForSavedTable); return; } //-now we can store the item if (m_newTable) { m_partItemForSavedTable->setIdentifier(m_destinationTableSchema->id()); project->addStoredItem(part->info(), m_partItemForSavedTable); } m_importingProgressBar->hide(); m_importProgressLabel->setText(xi18nc("@info", "Data has been successfully imported to table %1.", m_destinationTableSchema->name())); m_importInProgress = false; //qDebug()<<"IMPORT DONE"; KGuiItem::assign(finishButton(), KStandardGuiItem::open()); finishButton()->setEnabled(true); KGuiItem::assign(button(QDialogButtonBox::Cancel), KStandardGuiItem::close()); nextButton()->setEnabled(false); backButton()->setEnabled(false); m_conn = 0; d->imported = true; } void KexiCSVImportDialog::reject() { //qDebug()<<"IMP_P"<horizontalHeaderItem(col)->text(); if (header == xi18nc("Text type for column", "Text")) return TEXT; else if (header == xi18nc("Numeric type for column", "Number")) return NUMBER; else if (header == xi18nc("Currency type for column", "Currency")) return CURRENCY; else return DATE; } QString KexiCSVImportDialog::getText(int row, int col) { return m_table->item(row, col)->text(); } void KexiCSVImportDialog::ignoreDuplicatesChanged(int) { fillTable(); } void KexiCSVImportDialog::slot1stRowForFieldNamesChanged(int state) { m_adjustRows = true; if (m_1stRowForFieldNames->isChecked() && m_startline > 0 && m_startline >= (m_startAtLineSpinBox->maximum() - 1)) { m_startline--; } m_columnsAdjusted = false; fillTable(); m_table->setFirstRowForFieldNames(state); } void KexiCSVImportDialog::optionsButtonClicked() { KexiCSVImportOptionsDialog dlg(m_options, this); if (QDialog::Accepted != dlg.exec()) return; KexiCSVImportOptions newOptions(dlg.options()); if (m_options != newOptions) { m_options = newOptions; if (!openData()) return; fillTable(); } } bool KexiCSVImportDialog::eventFilter(QObject * watched, QEvent * e) { QEvent::Type t = e->type(); // temporary disable keyboard and mouse events for time-consuming tasks if (m_blockUserEvents && (t == QEvent::KeyPress || t == QEvent::KeyRelease || t == QEvent::MouseButtonPress || t == QEvent::MouseButtonDblClick || t == QEvent::Paint)) return true; if (watched == m_startAtLineSpinBox && t == QEvent::KeyPress) { QKeyEvent *ke = static_cast(e); if (ke->key() == Qt::Key_Enter || ke->key() == Qt::Key_Return) { m_tableView->setFocus(); return true; } } return QDialog::eventFilter(watched, e); } void KexiCSVImportDialog::slotPrimaryKeyFieldToggled(bool on) { setPrimaryKeyIcon(m_primaryKeyColumn, false); m_primaryKeyColumn = on ? m_tableView->currentIndex().column() : -1; setPrimaryKeyIcon(m_primaryKeyColumn, true); } void KexiCSVImportDialog::setPrimaryKeyIcon(int column, bool set) { if (column >= 0 && column < m_table->columnCount()) { m_table->setData(m_table->index(0, column), set ? m_pkIcon : QPixmap(), Qt::DecorationRole); } } void KexiCSVImportDialog::updateRowCountInfo() { m_infoLbl->setFileName(m_fname); if (m_allRowsLoadedInPreview) { m_infoLbl->setCommentText( xi18nc("row count", "(rows: %1)", m_table->rowCount() - 1 + m_startline)); m_infoLbl->commentLabel()->setToolTip(QString()); } else { m_infoLbl->setCommentText( xi18nc("row count", "(rows: more than %1)", m_table->rowCount() - 1 + m_startline)); m_infoLbl->commentLabel()->setToolTip(xi18n("Not all rows are visible on this preview")); } } QPushButton* KexiCSVImportDialog::configureButton() const { // Help button is used as Configure return button(QDialogButtonBox::Help); } diff --git a/src/plugins/importexport/csv/kexicsvimportdialog.h b/src/plugins/importexport/csv/kexicsvimportdialog.h index 191252857..0d130da1d 100644 --- a/src/plugins/importexport/csv/kexicsvimportdialog.h +++ b/src/plugins/importexport/csv/kexicsvimportdialog.h @@ -1,318 +1,318 @@ /* This file is part of the KDE project Copyright (C) 2005-2016 Jarosław Staniek Copyright (C) 2012 Oleg Kukharchuk This work is based on kspread/dialogs/kspread_dlg_csv.cc and could be merged back with Calligra Libraries. Copyright (C) 2002-2003 Norbert Andres Copyright (C) 2002-2003 Ariya Hidayat Copyright (C) 2002 Laurent Montel Copyright (C) 1999 David Faure This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #ifndef KEXI_CSVIMPORTDIALOG_H #define KEXI_CSVIMPORTDIALOG_H #include -#include +#include #include #include #include #include #include #include #include #include #include "kexicsvimportoptionsdlg.h" class QHBoxLayout; class QGridLayout; class QCheckBox; class QLabel; class QTableView; class QTreeView; class QFile; class QStackedWidget; class QProgressDialog; class QProgressBar; class QSpinBox; class KComboBox; class KPageWidgetItem; class KDbConnection; class KDbTableSchema; class KexiCSVCommentWidget; class KexiCSVDelimiterWidget; class KexiCSVTextQuoteComboBox; class KexiCSVInfoLabel; class KexiProject; class KexiCSVImportDialogModel; class KexiCSVImportDialogItemDelegate; class KexiFileWidget; class KexiCommandLinkButton; class KexiNameWidget; class KexiProjectNavigator; class KexiFieldListModel; namespace KexiPart { class Item; } /** * @short Kexi CSV import dialog * * This is temporary solution for Kexi CSV import, * based on kspread/dialogs/kspread_dlg_csv.h, cc. * * Provides dialog for managing CSV (comma separated value) data. * * Currently KexiCSVImportDialog is used for converting text into columns, * inserting text file and pasting text from clipboard, where conversion * from CSV (comma separated value) data is is all required. * The different purposed mentioned above is determined * using mode, which can be Column, File, or Clipboard respectively. */ class KexiCSVImportDialog : public KAssistantDialog { Q_OBJECT public: enum Mode { Clipboard, File /*, Column*/ }; enum Header { TEXT, NUMBER, DATE, CURRENCY }; //! @todo what about making it kexidb-independent? explicit KexiCSVImportDialog(Mode mode, QWidget *parent = 0); virtual ~KexiCSVImportDialog(); bool canceled() const; protected: virtual bool eventFilter(QObject *watched, QEvent *e); bool openData(); virtual void accept(); virtual void reject(); private: //! Used in emergency by accept() //! @note @a partItemForSavedTable is IN-OUT void dropDestinationTable(KexiProject* project, KexiPart::Item* &partItemForSavedTable); //! Used in emergency by accept() //! @note @a partItemForSavedTable is IN-OUT void raiseErrorInAccept(KexiProject* project, KexiPart::Item* &partItemForSavedTable); QGridLayout* MyDialogLayout; QHBoxLayout* Layout1; KexiCSVImportDialogModel *m_table; KexiCSVImportDialogItemDelegate *m_tableItemDelegate; QTableView *m_tableView; KexiCSVDelimiterWidget* m_delimiterWidget; KexiCSVCommentWidget* m_commentWidget; bool m_detectDelimiter; //!< true if delimiter should be detected //!< (true by default, set to false if user sets delimiter) QLabel* m_formatLabel; KComboBox* m_formatCombo; QSpinBox *m_startAtLineSpinBox; KexiCSVTextQuoteComboBox* m_comboQuote; QLabel* m_startAtLineLabel; QLabel* TextLabel2; QCheckBox* m_ignoreDuplicates; QCheckBox* m_1stRowForFieldNames; QCheckBox* m_primaryKeyField; KexiFileWidget *m_openFileWidget; QWidget *m_optionsWidget; QWidget *m_saveMethodWidget; KPageWidgetItem *m_openFilePage; KPageWidgetItem *m_optionsPage; KPageWidgetItem *m_saveMethodPage; KPageWidgetItem *m_chooseTablePage; KexiCommandLinkButton *m_newTableButton; KexiCommandLinkButton *m_existentTableButton; QStackedWidget *m_tableNameWidget; KPageWidgetItem *m_tableNamePage; KexiNameWidget *m_newTableWidget; KexiProjectNavigator *m_tablesList; QTreeView *m_fieldsListView; QLabel *m_tableCaptionLabel; QLabel *m_tableNameLabel; QLabel *m_recordCountLabel; QLabel *m_colCountLabel; QWidget *m_importWidget; KPageWidgetItem *m_importPage; KexiCSVInfoLabel *m_fromLabel; KexiCSVInfoLabel *m_toLabel; QLabel *m_importProgressLabel; void detectTypeAndUniqueness(int row, int col, const QString& text); void setText(int row, int col, const QString& text, bool inGUI); /*! Parses date from \a text and stores into \a date. m_dateRegExp is used for clever detection; if '/' separated is found, it's assumed the format is american mm/dd/yyyy. This function supports omitted zeros, so 1/2/2006 is parsed properly too. \return true on success. */ bool parseDate(const QString& text, QDate& date); /*! Parses time from \a text and stores into \a date. m_timeRegExp1 and m_timeRegExp2 are used for clever detection; both hh:mm:ss and hh:mm are supported. This function supports omitted zeros, so 1:2:3 is parsed properly too. \return true on success. */ bool parseTime(const QString& text, QTime& time); /*! Called after the first fillTable() when number of rows is unknown. */ void adjustRows(int iRows); int getHeader(int col); QString getText(int row, int col); void updateColumn(int col); bool isPrimaryKeyAllowed(int col); void setPrimaryKeyIcon(int column, bool set); void updateRowCountInfo(); tristate loadRows(QString &field, int &row, int &columnm, int &maxColumn, bool inGUI); /*! Detects delimiter by looking at first 4K bytes of the data. Used by loadRows(). The used algorithm: 1. Look byte by byte and locate special characters that can be delimiters. Special fact is taken into account: if there are '"' quotes used for text values, delimiters that follow directly the closing quote has higher priority than the one that follows other character. We do not assume that every text value is quoted. Summing up, there is following hierarchy (from highest to lowest): quote+tab, quote+semicolon, quote+comma, tab, semicolon, comma. Space characters are skipped. Text inside quotes is skipped, as well as double (escaped) quotes. 2. While scanning the data, for every row following number of tabs, semicolons and commas (only these outside of the quotes) are computed. On every line the values are appended to a separate list (QList). 3. After scanning, all the values are checked on the QList of tabs. If the list has more one element (so there was more than one row) and all the values (numbers of tabs) are equal, it's very probable the tab is a delimiter. So, this character is returned as a delimiter. 3a. The same algorithm as in 3. is performed for semicolon character. 3b. The same algorithm as in 3. is performed for comma character. 4. If the step 3. did not return a delimiter, a character found in step 1. with the highest priority is retured as delimiter. */ QString detectDelimiterByLookingAtFirstBytesOfFile(QTextStream *inputStream); /*! Callback, called whenever row is loaded in loadRows(). When inGUI is true, nothing is performed, else database buffer is written back to the database. */ bool saveRow(bool inGUI); //! @return date built out of @a y, @a m, @a d parts, //! taking m_minimumYearFor100YearSlidingWindow into account QDate buildDate(int y, int m, int d) const; //! Updates size of m_columnNames and m_changedColumnNames if needed void updateColumnVectorSize(); QPushButton* configureButton() const; bool m_parseComments; bool m_canceled; bool m_adjustRows; int m_startline; QChar m_textquote; QChar m_commentSymbol; QString m_clipboardData; QByteArray m_fileArray; Mode m_mode; - QRegExp m_dateRegExp, m_timeRegExp1, m_timeRegExp2, m_fpNumberRegExp1, m_fpNumberRegExp2; + QRegularExpression m_dateRegExp, m_timeRegExp1, m_timeRegExp2, m_fpNumberRegExp1, m_fpNumberRegExp2; bool m_columnsAdjusted; //!< to call adjustColumn() only once bool m_1stRowForFieldNamesDetected; //!< used to force rerun fillTable() after 1st row bool m_firstFillTableCall; //!< used to know whether it's 1st fillTable() call bool m_blockUserEvents; int m_primaryKeyColumn; //!< index of column with PK assigned (-1 if none) int m_maximumRowsForPreview; int m_maximumBytesForPreview; /*! The minimum year for the "100 year sliding date window": range of years that defines where any year expressed as two digits falls. Example: for date window from 1930 to 2029, two-digit years between 0 and 29 fall in the 2000s, and two-digit years between 30 and 99 fall in the 1900s. The default is 1930. */ int m_minimumYearFor100YearSlidingWindow; QPixmap m_pkIcon; QString m_fname; QFile* m_file; QTextStream *m_inputStream; //!< used in loadData() KexiCSVImportOptions m_options; QProgressDialog *m_loadingProgressDlg; QProgressBar *m_importingProgressBar; bool m_dialogCanceled; KexiCSVInfoLabel *m_infoLbl; KDbConnection *m_conn; //!< (temp) database connection used for importing KexiFieldListModel *m_fieldsListModel; KDbTableSchema *m_destinationTableSchema; //!< (temp) dest. table schema used for importing KDbPreparedStatement m_importingStatement; QList m_dbRowBuffer; //!< (temp) used for importing bool m_implicitPrimaryKeyAdded; //!< (temp) used for importing bool m_allRowsLoadedInPreview; //!< we need to know whether all rows were loaded or it's just a partial data preview bool m_stoppedAt_MAX_BYTES_TO_PREVIEW; //!< used to compute m_allRowsLoadedInPreview const QString m_stringNo, m_stringI18nNo, m_stringFalse, m_stringI18nFalse; //!< used for importing boolean values int m_prevColumnForSetText; //!< used for non-gui tracking of skipped clolumns, //!< so can be saved to the database, //!< e.g. first three columns are saved for ,,,"abc" line in the CSV data QElapsedTimer m_elapsedTimer; //!< Used to update progress qint64 m_elapsedMs; void createImportMethodPage(); void createOptionsPage(); void createFileOpenPage(); void createTableNamePage(); void createImportPage(); bool m_newTable; KDbPreparedStatementParameters m_valuesToInsert; KexiPart::Item* m_partItemForSavedTable; bool m_importInProgress; bool m_importCanceled; class Private; Private * const d; public Q_SLOTS: virtual void next(); private Q_SLOTS: void fillTable(); void fillTableLater(); void initLater(); void formatChanged(int id); void delimiterChanged(const QString& delimiter); void commentSymbolChanged(const QString& commentSymbol); void startlineSelected(int line); void textquoteSelected(int); void currentCellChanged(const QModelIndex &cur, const QModelIndex &prev); void ignoreDuplicatesChanged(int); void slot1stRowForFieldNamesChanged(int state); void optionsButtonClicked(); void slotPrimaryKeyFieldToggled(bool on); void slotCurrentPageChanged(KPageWidgetItem *page, KPageWidgetItem *prev); void slotCommandLinkClicked(); void slotShowSchema(KexiPart::Item *item); void import(); }; #endif diff --git a/src/plugins/queries/kexiquerydesignerguieditor.cpp b/src/plugins/queries/kexiquerydesignerguieditor.cpp index 3476ab8f2..a0478d84a 100644 --- a/src/plugins/queries/kexiquerydesignerguieditor.cpp +++ b/src/plugins/queries/kexiquerydesignerguieditor.cpp @@ -1,1896 +1,1896 @@ /* This file is part of the KDE project Copyright (C) 2004 Lucijan Busch Copyright (C) 2004-2016 Jarosław Staniek This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "kexiquerydesignerguieditor.h" #include #include #include #include //! @todo KEXI3 Port #include #include #include #include #include #include #include #include #include "kexiquerypart.h" #include "kexiqueryview.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include -#include +#include #include #include #include #include #include #include #include //! @todo remove KEXI_NO_QUERY_TOTALS later #define KEXI_NO_QUERY_TOTALS //! indices for table columns #define COLUMN_ID_COLUMN 0 #define COLUMN_ID_TABLE 1 #define COLUMN_ID_VISIBLE 2 #ifdef KEXI_NO_QUERY_TOTALS # define COLUMN_ID_SORTING 3 # define COLUMN_ID_CRITERIA 4 #else # define COLUMN_ID_TOTALS 3 # define COLUMN_ID_SORTING 4 # define COLUMN_ID_CRITERIA 5 #endif /*! @internal */ class KexiQueryDesignerGuiEditor::Private { public: Private(KexiQueryDesignerGuiEditor *p) : q(p) , conn(0) { droppedNewRecord = 0; slotTableAdded_enabled = true; sortColumnPreferredWidth = 0; } bool changeSingleCellValue(KDbRecordData *recordData, int columnNumber, const QVariant& value, KDbResultInfo* result) { data->clearRecordEditBuffer(); if (!data->updateRecordEditBuffer(recordData, columnNumber, value) || !data->saveRecordChanges(recordData, true)) { if (result) *result = data->result(); return false; } return true; } KexiQueryDesignerGuiEditor *q; KDbTableViewData *data; KexiDataTableView *dataTable; //! @todo KEXI3 use equivalent of QPointer KDbConnection *conn; KexiRelationsView *relations; KexiSectionHeader *head; QSplitter *spl; /*! Used to remember in slotDroppedAtRow() what data was dropped, so we can create appropriate prop. set in slotRecordInserted() This information is cached and entirely refreshed on updateColumnsData(). */ KDbTableViewData *fieldColumnData, *tablesColumnData; /*! Collects identifiers selected in 1st (field) column, so we're able to distinguish between table identifiers selected from the dropdown list, and strings (e.g. expressions) entered by hand. This information is cached and entirely refreshed on updateColumnsData(). The dict is filled with (char*)1 values (doesn't matter what it is); */ QSet fieldColumnIdentifiers; void addFieldColumnIdentifier(const QString& id) { fieldColumnIdentifiers.insert(id.toLower()); } int comboArrowWidth; int sortColumnPreferredWidth; void initSortColumnPreferredWidth(const QVector &items) { int maxw = -1; for (int i=0; i < items.size(); ++i) { maxw = qMax(maxw, q->fontMetrics().width(items[i] + QLatin1String(" "))); } sortColumnPreferredWidth = maxw + KexiUtils::comboBoxArrowSize(q->style()).width(); } KexiDataAwarePropertySet* sets; KDbRecordData *droppedNewRecord; QString droppedNewTable, droppedNewField; bool slotTableAdded_enabled; }; static bool isAsterisk(const QString& tableName, const QString& fieldName) { return tableName == "*" || fieldName.endsWith('*'); } //! @internal \return true if sorting is allowed for \a fieldName and \a tableName static bool sortingAllowed(const QString& fieldName, const QString& tableName) { return !(fieldName == "*" || (fieldName.isEmpty() && tableName == "*")); } //========================================================= KexiQueryDesignerGuiEditor::KexiQueryDesignerGuiEditor( QWidget *parent) : KexiView(parent) , d(new Private(this)) { d->conn = KexiMainWindowIface::global()->project()->dbConnection(); d->spl = new QSplitter(Qt::Vertical, this); d->spl->setChildrenCollapsible(false); d->relations = new KexiRelationsView(d->spl); d->spl->addWidget(d->relations); d->relations->setObjectName("relations"); connect(d->relations, SIGNAL(tableAdded(KDbTableSchema*)), this, SLOT(slotTableAdded(KDbTableSchema*))); connect(d->relations, SIGNAL(tableHidden(KDbTableSchema*)), this, SLOT(slotTableHidden(KDbTableSchema*))); connect(d->relations, SIGNAL(appendFields(KDbTableOrQuerySchema&,QStringList)), this, SLOT(slotAppendFields(KDbTableOrQuerySchema&,QStringList))); d->head = new KexiSectionHeader(xi18n("Query Columns"), Qt::Vertical, d->spl); d->spl->addWidget(d->head); d->dataTable = new KexiDataTableView(d->head, false); d->head->setWidget(d->dataTable); d->dataTable->setObjectName("guieditor_dataTable"); d->dataTable->dataAwareObject()->setSpreadSheetMode(true); d->data = new KDbTableViewData(); //just empty data d->sets = new KexiDataAwarePropertySet(this, d->dataTable->dataAwareObject()); connect(d->sets, SIGNAL(propertyChanged(KPropertySet&,KProperty&)), this, SLOT(slotPropertyChanged(KPropertySet&,KProperty&))); initTableColumns(); initTableRows(); QList c; c << COLUMN_ID_COLUMN << COLUMN_ID_TABLE << COLUMN_ID_CRITERIA; if (d->dataTable->tableView()/*sanity*/) { d->dataTable->tableView()->adjustColumnWidthToContents(COLUMN_ID_VISIBLE); d->dataTable->tableView()->setColumnWidth(COLUMN_ID_SORTING, d->sortColumnPreferredWidth); d->dataTable->tableView()->setStretchLastColumn(true); d->dataTable->tableView()->maximizeColumnsWidth(c); d->dataTable->tableView()->setDropsAtRecordEnabled(true); connect(d->dataTable->tableView(), SIGNAL(dragOverRecord(KDbRecordData*,int,QDragMoveEvent*)), this, SLOT(slotDragOverTableRecord(KDbRecordData*,int,QDragMoveEvent*))); connect(d->dataTable->tableView(), SIGNAL(droppedAtRecord(KDbRecordData*,int,QDropEvent*,KDbRecordData*&)), this, SLOT(slotDroppedAtRecord(KDbRecordData*,int,QDropEvent*,KDbRecordData*&))); connect(d->dataTable->tableView(), SIGNAL(newItemAppendedForAfterDeletingInSpreadSheetMode()), this, SLOT(slotNewItemAppendedForAfterDeletingInSpreadSheetMode())); } connect(d->data, SIGNAL(aboutToChangeCell(KDbRecordData*,int,QVariant*,KDbResultInfo*)), this, SLOT(slotBeforeCellChanged(KDbRecordData*,int,QVariant*,KDbResultInfo*))); connect(d->data, SIGNAL(recordInserted(KDbRecordData*,int,bool)), this, SLOT(slotRecordInserted(KDbRecordData*,int,bool))); connect(d->relations, SIGNAL(tablePositionChanged(KexiRelationsTableContainer*)), this, SLOT(slotTablePositionChanged(KexiRelationsTableContainer*))); connect(d->relations, SIGNAL(aboutConnectionRemove(KexiRelationsConnection*)), this, SLOT(slotAboutConnectionRemove(KexiRelationsConnection*))); addChildView(d->relations); addChildView(d->dataTable); setViewWidget(d->spl, false/* no focus proxy*/); setFocusProxy(d->dataTable); d->relations->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); d->head->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); updateGeometry(); d->spl->setSizes(QList() << 800 << 400); } KexiQueryDesignerGuiEditor::~KexiQueryDesignerGuiEditor() { delete d; } void KexiQueryDesignerGuiEditor::initTableColumns() { KDbTableViewColumn *col1 = new KDbTableViewColumn("column", KDbField::Enum, xi18n("Column"), xi18n("Describes field name or expression for the designed query.")); col1->setRelatedDataEditable(true); d->fieldColumnData = new KDbTableViewData(KDbField::Text, KDbField::Text); col1->setRelatedData(d->fieldColumnData); d->data->addColumn(col1); KDbTableViewColumn *col2 = new KDbTableViewColumn("table", KDbField::Enum, xi18n("Table"), xi18n("Describes table for a given field. Can be empty.")); d->tablesColumnData = new KDbTableViewData(KDbField::Text, KDbField::Text); col2->setRelatedData(d->tablesColumnData); d->data->addColumn(col2); KDbTableViewColumn *col3 = new KDbTableViewColumn("visible", KDbField::Boolean, xi18n("Visible"), xi18n("Describes visibility for a given field or expression.")); col3->field()->setDefaultValue(QVariant(false)); col3->field()->setNotNull(true); d->data->addColumn(col3); #ifndef KEXI_NO_QUERY_TOTALS KDbTableViewColumn *col4 = new KDbTableViewColumn("totals", KDbField::Enum, futureI18n("Totals"), futureI18n("Describes a way of computing totals for a given field or expression.")); QVector totalsTypes; totalsTypes.append(futureI18n("Group by")); totalsTypes.append(futureI18n("Sum")); totalsTypes.append(futureI18n("Average")); totalsTypes.append(futureI18n("Min")); totalsTypes.append(futureI18n("Max")); //! @todo more like this col4->field()->setEnumHints(totalsTypes); d->data->addColumn(col4); #endif KDbTableViewColumn *col5 = new KDbTableViewColumn("sort", KDbField::Enum, xi18n("Sorting"), xi18n("Describes a way of sorting for a given field.")); QVector sortTypes; sortTypes.append(""); sortTypes.append(xi18n("Ascending")); sortTypes.append(xi18n("Descending")); col5->field()->setEnumHints(sortTypes); d->data->addColumn(col5); d->initSortColumnPreferredWidth(sortTypes); KDbTableViewColumn *col6 = new KDbTableViewColumn("criteria", KDbField::Text, xi18n("Criteria"), xi18n("Describes the criteria for a given field or expression.")); d->data->addColumn(col6); } void KexiQueryDesignerGuiEditor::initTableRows() { d->data->deleteAllRecords(); for (int i = 0; i < (int)d->sets->size(); i++) { KDbRecordData* data = d->data->createItem(); d->data->append(data); (*data)[COLUMN_ID_VISIBLE] = QVariant(false); } d->dataTable->dataAwareObject()->setData(d->data); updateColumnsData(); } void KexiQueryDesignerGuiEditor::updateColumnsData() { d->dataTable->dataAwareObject()->acceptRecordEditing(); QStringList sortedTableNames; foreach(KexiRelationsTableContainer* cont, *d->relations->tables()) { sortedTableNames += cont->schema()->name(); } qSort(sortedTableNames); //several tables can be hidden now, so remove rows for these tables QList recordsToDelete; for (int r = 0; r < (int)d->sets->size(); r++) { KPropertySet *set = d->sets->at(r); if (set) { QString tableName = (*set)["table"].value().toString(); QString fieldName = (*set)["field"].value().toString(); const bool allTablesAsterisk = tableName == "*" && d->relations->tables()->isEmpty(); const bool fieldNotFound = tableName != "*" && !(*set)["isExpression"].value().toBool() && sortedTableNames.end() == qFind(sortedTableNames.begin(), sortedTableNames.end(), tableName); if (allTablesAsterisk || fieldNotFound) { //table not found: mark this line for later removal recordsToDelete += r; } } } d->data->deleteRecords(recordsToDelete); //update 'table' and 'field' columns d->tablesColumnData->deleteAllRecords(); d->fieldColumnData->deleteAllRecords(); d->fieldColumnIdentifiers.clear(); KDbRecordData *data = d->fieldColumnData->createItem(); (*data)[COLUMN_ID_COLUMN] = "*"; (*data)[COLUMN_ID_TABLE] = "*"; d->fieldColumnData->append(data); d->addFieldColumnIdentifier((*data)[COLUMN_ID_COLUMN].toString()); //cache tempData()->unregisterForTablesSchemaChanges(); foreach(const QString& tableName, sortedTableNames) { //table /*! @todo what about query? */ KDbTableSchema *table = d->relations->tables()->value(tableName)->schema()->table(); d->conn->registerForTableSchemaChanges(tempData(), table); //this table will be used data = d->tablesColumnData->createItem(); (*data)[COLUMN_ID_COLUMN] = table->name(); (*data)[COLUMN_ID_TABLE] = (*data)[COLUMN_ID_COLUMN]; d->tablesColumnData->append(data); //fields data = d->fieldColumnData->createItem(); (*data)[COLUMN_ID_COLUMN] = QString(table->name() + ".*"); (*data)[COLUMN_ID_TABLE] = (*data)[COLUMN_ID_COLUMN]; d->fieldColumnData->append(data); d->addFieldColumnIdentifier((*data)[COLUMN_ID_COLUMN].toString()); //cache foreach(KDbField *field, *table->fields()) { data = d->fieldColumnData->createItem(); (*data)[COLUMN_ID_COLUMN] = QString(table->name() + '.' + field->name()); (*data)[COLUMN_ID_TABLE] = QString(" " + field->name()); d->fieldColumnData->append(data); d->addFieldColumnIdentifier((*data)[COLUMN_ID_COLUMN].toString()); //cache } } //! @todo } KexiRelationsView *KexiQueryDesignerGuiEditor::relationsView() const { return d->relations; } KexiQueryPart::TempData * KexiQueryDesignerGuiEditor::tempData() const { return static_cast(window()->data()); } static QString msgCannotSwitch_EmptyDesign() { return xi18n("Cannot switch to data view, because query design is empty.\n" "First, please create your design."); } bool KexiQueryDesignerGuiEditor::buildSchema(QString *errMsg) { //build query schema KexiQueryPart::TempData * temp = tempData(); if (temp->query()) { KexiQueryView *queryDataView = dynamic_cast(window()->viewForMode(Kexi::DataViewMode)); if (queryDataView) { queryDataView->setData(0); } temp->clearQuery(); } else { temp->setQuery(new KDbQuerySchema()); } //add tables foreach(KexiRelationsTableContainer* cont, *d->relations->tables()) { /*! @todo what about query? */ temp->query()->addTable(cont->schema()->table()); } //add fields, also build: // -WHERE expression // -ORDER BY list KDbExpression whereExpr; const int count = qMin(d->data->count(), d->sets->size()); bool fieldsFound = false; KDbTableViewDataConstIterator it(d->data->constBegin()); for (int i = 0; i < count && it != d->data->constEnd(); ++it, i++) { if (!(**it)[COLUMN_ID_TABLE].isNull() && (**it)[COLUMN_ID_COLUMN].isNull()) { //show message about missing field name, and set focus to that cell qDebug() << "no field provided!"; d->dataTable->dataAwareObject()->setCursorPosition(i, 0); if (errMsg) *errMsg = xi18nc("@info", "Select column for table %1", (**it)[COLUMN_ID_TABLE].toString()); return false; } KPropertySet *set = d->sets->at(i); if (set) { QString tableName = (*set)["table"].value().toString().trimmed(); QString fieldName = (*set)["field"].value().toString(); QString fieldAndTableName = fieldName; KDbField *currentField = 0; // will be set if this column is a single field if (!tableName.isEmpty()) fieldAndTableName.prepend(tableName + "."); const bool fieldVisible = (*set)["visible"].value().toBool(); QString criteriaStr = (*set)["criteria"].value().toString(); QByteArray alias((*set)["alias"].value().toByteArray()); if (!criteriaStr.isEmpty()) { KDbToken token; KDbExpression criteriaExpr = parseExpressionString(criteriaStr, &token, true/*allowRelationalOperator*/); if (criteriaExpr.isValid()) {//for sanity if (errMsg) *errMsg = xi18nc("@info", "Invalid criteria %1", criteriaStr); return false; } //build relational expression for column variable KDbVariableExpression varExpr(fieldAndTableName); criteriaExpr = KDbBinaryExpression(varExpr, token, criteriaExpr); //critera ok: add it to WHERE section if (whereExpr.isValid()) whereExpr = KDbBinaryExpression(whereExpr, KDbToken::AND, criteriaExpr); else //first expr. whereExpr = criteriaExpr; } if (tableName.isEmpty()) { if ((*set)["isExpression"].value().toBool() == true) { //add expression column KDbToken dummyToken; KDbExpression columnExpr = parseExpressionString(fieldName, &dummyToken, false/*!allowRelationalOperator*/); if (!columnExpr.isValid()) { if (errMsg) *errMsg = xi18nc("@info", "Invalid expression %1", fieldName); return false; } temp->query()->addExpression(columnExpr, fieldVisible); if (fieldVisible) fieldsFound = true; if (!alias.isEmpty()) temp->query()->setColumnAlias(temp->query()->fieldCount() - 1, alias); } //! @todo } else if (tableName == "*") { //all tables asterisk if (!temp->query()->addAsterisk(new KDbQueryAsterisk(temp->query(), 0), fieldVisible)) { return false; } if (fieldVisible) fieldsFound = true; continue; } else { KDbTableSchema *t = d->conn->tableSchema(tableName); if (fieldName == "*") { //single-table asterisk: + ".*" + number if (!temp->query()->addAsterisk(new KDbQueryAsterisk(temp->query(), t), fieldVisible)) { return false; } if (fieldVisible) fieldsFound = true; } else { if (!t) { qWarning() << "query designer: NO TABLE '" << (*set)["table"].value().toString() << "'"; continue; } currentField = t->field(fieldName); if (!currentField) { qWarning() << "query designer: NO FIELD '" << fieldName << "'"; continue; } if (!fieldVisible && criteriaStr.isEmpty() && set->contains("isExpression") && (*set)["sorting"].value().toString() != "nosorting") { qDebug() << "invisible field with sorting: do not add it to the fields list"; continue; } const int tablePosition = temp->query()->tablePosition(t->name()); if (!temp->query()->addField(currentField, tablePosition, fieldVisible)) { return false; } if (fieldVisible) fieldsFound = true; if (!alias.isEmpty()) temp->query()->setColumnAlias(temp->query()->fieldCount() - 1, alias); } } } else {//!set //qDebug() << (**it)[COLUMN_ID_TABLE].toString(); } } if (!fieldsFound) { if (errMsg) *errMsg = msgCannotSwitch_EmptyDesign(); return false; } if (whereExpr.isValid()) { qDebug() << "setting CRITERIA:" << whereExpr; } //set always, because if whereExpr==NULL, //this will clear prev. expr temp->query()->setWhereExpression(whereExpr); //add relations (looking for connections) foreach(KexiRelationsConnection* conn, *d->relations->relationsConnections()) { KexiRelationsTableContainer *masterTable = conn->masterTable(); KexiRelationsTableContainer *detailsTable = conn->detailsTable(); /*! @todo what about query? */ temp->query()->addRelationship( masterTable->schema()->table()->field(conn->masterField()), detailsTable->schema()->table()->field(conn->detailsField())); } // Add sorting information (ORDER BY) - we can do that only now // after all KDbQueryColumnInfo items are instantiated KDbOrderByColumnList orderByColumns; it = d->data->constBegin(); int fieldNumber = -1; //field number (empty rows are omitted) for (int i = 0/*row number*/; i < count && it != d->data->constEnd(); ++it, i++) { KPropertySet *set = d->sets->at(i); if (!set) continue; fieldNumber++; KDbField *currentField = 0; KDbQueryColumnInfo *currentColumn = 0; QString sortingString((*set)["sorting"].value().toString()); if (sortingString != "ascending" && sortingString != "descending") continue; if (!(*set)["visible"].value().toBool()) { // this row defines invisible field but contains sorting information, // what means KDbField should be used as a reference for this sorting // Note1: alias is not supported here. // Try to find a field (not mentioned after SELECT): currentField = temp->query()->findTableField((*set)["field"].value().toString()); if (!currentField) { qWarning() << "NO FIELD" << (*set)["field"].value().toString() << "available for sorting"; continue; } orderByColumns.appendField(currentField, sortingString == "ascending"); continue; } currentField = temp->query()->field(fieldNumber); if (!currentField || currentField->isExpression() || currentField->isQueryAsterisk()) //! @todo support expressions here continue; //! @todo ok, but not for expressions QString aliasString((*set)["alias"].value().toString()); currentColumn = temp->query()->columnInfo( (*set)["table"].value().toString() + "." + (aliasString.isEmpty() ? currentField->name() : aliasString)); if (currentField && currentColumn) { if (currentColumn->visible) orderByColumns.appendColumn(currentColumn, sortingString == "ascending"); else if (currentColumn->field) orderByColumns.appendField(currentColumn->field, sortingString == "ascending"); } } temp->query()->setOrderByColumnList(orderByColumns); qDebug() << *temp->query(); temp->registerTableSchemaChanges(temp->query()); //! @todo ? return true; } tristate KexiQueryDesignerGuiEditor::beforeSwitchTo(Kexi::ViewMode mode, bool *dontStore) { Q_ASSERT(dontStore); qDebug() << mode; if (!d->dataTable->dataAwareObject()->acceptRecordEditing()) return cancelled; qDebug() << "queryChangedInView:" << tempData()->queryChangedInView(); if (mode == Kexi::DesignViewMode) { return true; } else if (mode == Kexi::DataViewMode) { if (!isDirty() && window()->neverSaved()) { KMessageBox::information(this, msgCannotSwitch_EmptyDesign()); return cancelled; } if (tempData()->queryChangedInView() != Kexi::NoViewMode || !tempData()->query()) { //remember current design in a temporary structure QString errMsg; //build schema; problems are not allowed if (!buildSchema(&errMsg)) { KMessageBox::sorry(this, errMsg); return cancelled; } } *dontStore = true; //! @todo return true; } else if (mode == Kexi::TextViewMode) { *dontStore = true; if (tempData()->queryChangedInView() != Kexi::NoViewMode || !tempData()->query()) { //remember current design in a temporary structure //build schema; ignore problems buildSchema(); } /* if (tempData()->query && tempData()->query->fieldCount()==0) { //no fields selected: let's add "*" (all-tables asterisk), // otherwise SQL statement will be invalid tempData()->query->addAsterisk( new KDbQueryAsterisk( tempData()->query ) ); }*/ //! @todo return true; } return false; } tristate KexiQueryDesignerGuiEditor::afterSwitchFrom(Kexi::ViewMode mode) { if (!d->relations->setConnection(d->conn)) { window()->setStatus(d->conn); return false; } if (mode == Kexi::NoViewMode || (mode == Kexi::DataViewMode && !tempData()->query())) { //this is not a SWITCH but a fresh opening in this view mode if (!window()->neverSaved()) { if (!loadLayout()) { //err msg window()->setStatus(d->conn, xi18n("Query definition loading failed."), xi18n("Query design may be corrupted so it could not be opened even in text view.\n" "You can delete the query and create it again.")); return false; } // Invalid queries case: // KexiWindow::switchToViewMode() first opens DesignViewMode, // and then KexiQueryPart::loadSchemaObject() doesn't allocate KDbQuerySchema object // do we're carefully looking at window()->schemaObject() KDbQuerySchema * q = dynamic_cast(window()->schemaObject()); if (q) { KDbResultInfo result; showFieldsForQuery(q, result); if (!result.success) { window()->setStatus(&result, xi18n("Query definition loading failed.")); tempData()->proposeOpeningInTextViewModeBecauseOfProblems = true; return false; } } //! @todo load global query properties } } else if (mode == Kexi::TextViewMode || mode == Kexi::DataViewMode) { // Switch from text or data view. In the second case, the design could be changed as well // because there could be changes made in the text view before switching to the data view. if (tempData()->queryChangedInView() == Kexi::TextViewMode) { //SQL view changed the query design //-clear and regenerate GUI items initTableRows(); //! @todo if (tempData()->query()) { //there is a query schema to show showTablesForQuery(tempData()->query()); //-show fields KDbResultInfo result; showFieldsAndRelationsForQuery(tempData()->query(), result); if (!result.success) { window()->setStatus(&result, xi18n("Query definition loading failed.")); return false; } } else { d->relations->clear(); } } //! @todo load global query properties } if (mode == Kexi::DataViewMode) { //this is just a SWITCH from data view //set cursor if needed: if (d->dataTable->dataAwareObject()->currentRecord() < 0 || d->dataTable->dataAwareObject()->currentColumn() < 0) { d->dataTable->dataAwareObject()->ensureCellVisible(0, 0); d->dataTable->dataAwareObject()->setCursorPosition(0, 0); } } if (d->sets->size() > 0) { d->dataTable->tableView()->adjustColumnWidthToContents(COLUMN_ID_COLUMN); d->dataTable->tableView()->adjustColumnWidthToContents(COLUMN_ID_TABLE); } tempData()->setQueryChangedInView(false); setFocus(); //to allow shared actions proper update return true; } KDbObject* KexiQueryDesignerGuiEditor::storeNewData(const KDbObject& object, KexiView::StoreNewDataOptions options, bool *cancel) { Q_ASSERT(cancel); Q_UNUSED(options); if (!d->dataTable->dataAwareObject()->acceptRecordEditing()) { *cancel = true; return 0; } QString errMsg; KexiQueryPart::TempData * temp = tempData(); if (!temp->query() || !(viewMode() == Kexi::DesignViewMode && temp->queryChangedInView() == Kexi::NoViewMode)) { //only rebuild schema if it has not been rebuilt previously if (!buildSchema(&errMsg)) { KMessageBox::sorry(this, errMsg); *cancel = true; return 0; } } (KDbObject&)*temp->query() = object; //copy main attributes bool ok = d->conn->storeNewObjectData(temp->query()); if (ok) { ok = KexiMainWindowIface::global()->project()->removeUserDataBlock(temp->query()->id()); // for sanity } window()->setId(temp->query()->id()); if (ok) ok = storeLayout(); if (!ok) { temp->setQuery(0); return 0; } return temp->takeQuery(); //will be returned, so: don't keep it in temp } tristate KexiQueryDesignerGuiEditor::storeData(bool dontAsk) { if (!d->dataTable->dataAwareObject()->acceptRecordEditing()) return cancelled; const bool was_dirty = isDirty(); tristate res = KexiView::storeData(dontAsk); //this clears dirty flag if (true == res) res = buildSchema(); if (true == res) res = storeLayout(); if (true != res) { if (was_dirty) setDirty(true); } return res; } void KexiQueryDesignerGuiEditor::showTablesForQuery(KDbQuerySchema *query) { // instead of hiding all tables and showing some tables, // show only these new and hide these unncecessary; the same for connections) d->slotTableAdded_enabled = false; //speedup d->relations->removeAllConnections(); //connections will be recreated d->relations->hideAllTablesExcept(query->tables()); foreach(KDbTableSchema* table, *query->tables()) { d->relations->addTable(table); } d->slotTableAdded_enabled = true; updateColumnsData(); } void KexiQueryDesignerGuiEditor::addConnection( KDbField *masterField, KDbField *detailsField) { SourceConnection conn; conn.masterTable = masterField->table()->name(); //<<name(); conn.detailsTable = detailsField->table()->name(); conn.detailsField = detailsField->name(); d->relations->addConnection(conn); } void KexiQueryDesignerGuiEditor::showFieldsForQuery(KDbQuerySchema *query, KDbResultInfo& result) { showFieldsOrRelationsForQueryInternal(query, true, false, result); } void KexiQueryDesignerGuiEditor::showRelationsForQuery(KDbQuerySchema *query, KDbResultInfo& result) { showFieldsOrRelationsForQueryInternal(query, false, true, result); } void KexiQueryDesignerGuiEditor::showFieldsAndRelationsForQuery(KDbQuerySchema *query, KDbResultInfo& result) { showFieldsOrRelationsForQueryInternal(query, true, true, result); } void KexiQueryDesignerGuiEditor::showFieldsOrRelationsForQueryInternal( KDbQuerySchema *query, bool showFields, bool showRelations, KDbResultInfo& result) { result.clear(); const bool was_dirty = isDirty(); //1. Show explicitly declared relations: if (showRelations) { foreach(KDbRelationship *rel, *query->relationships()) { //! @todo: now only sigle-field relationships are implemented! KDbField *masterField = rel->masterIndex()->fields()->first(); KDbField *detailsField = rel->detailsIndex()->fields()->first(); addConnection(masterField, detailsField); } } //2. Collect information about criterias // --this must be top level chain of AND's // --this will also show joins as: [table1.]field1 = [table2.]field2 KDbUtils::CaseInsensitiveHash criterias; KDbExpression e = query->whereExpression(); KDbExpression eItem; while (e.isValid()) { //eat parentheses because the expression can be (....) AND (... AND ... ) while (e.isValid() && e.isUnary() && e.token() == '(') e = e.toUnary().arg(); if (e.isBinary() && e.token() == KDbToken::AND) { eItem = e.toBinary().left(); e = e.toBinary().right(); } else { eItem = e; e = KDbExpression(); } //eat parentheses while (eItem.isValid() && eItem.isUnary() && eItem.token() == '(') eItem = eItem.toUnary().arg(); if (!eItem.isValid()) continue; qDebug() << eItem; KDbBinaryExpression binary(eItem.toBinary()); if (binary.isValid() && eItem.expressionClass() == KDb::RelationalExpression) { KDbField *leftField = 0, *rightField = 0; if (eItem.token() == '=' && binary.left().isVariable() && binary.right().isVariable() && (leftField = query->findTableField(binary.left().toString(0).toString())) && (rightField = query->findTableField(binary.right().toString(0).toString()))) { //! @todo move this check to parser on KDbQuerySchema creation //! or to KDbQuerySchema creation (WHERE expression should be then simplified //! by removing joins //this is relationship defined as following JOIN: [table1.]field1 = [table2.]field2 if (showRelations) { //! @todo testing primary key here is too simplified; maybe look ar isForeignKey() or indices.. //! @todo what about multifield joins? if (leftField->isPrimaryKey()) addConnection(leftField /*master*/, rightField /*details*/); else addConnection(rightField /*master*/, leftField /*details*/); //! @todo addConnection() should have "bool oneToOne" arg, for 1-to-1 relations } } else if (binary.left().isVariable()) { //this is: variable , op , argument //store variable -> argument: criterias.insertMulti(binary.left().toVariable().name(), binary.right()); } else if (binary.right().isVariable()) { //this is: argument , op , variable //store variable -> argument: criterias.insertMulti(binary.right().toVariable().name(), binary.left()); } } } //while if (!showFields) return; //3. show fields (including * and table.*) int row_num = 0; QSet usedCriterias; // <-- used criterias will be saved here // so in step 4. we will be able to add // remaining invisible columns with criterias qDebug() << *query; foreach(KDbField* field, *query->fields()) { qDebug() << *field; } foreach(KDbField* field, *query->fields()) { //append a new row QString tableName, fieldName, columnAlias, criteriaString; KDbBinaryExpression criteriaExpr; KDbExpression criteriaArgument; if (field->isQueryAsterisk()) { if (field->table()) {//single-table asterisk tableName = field->table()->name(); fieldName = "*"; } else {//all-tables asterisk tableName = "*"; fieldName = ""; } } else { columnAlias = query->columnAlias(row_num); if (field->isExpression()) { //! @todo ok? perhaps do not allow to omit aliases? fieldName = field->expression().toString(0).toString(); } else { tableName = field->table()->name(); fieldName = field->name(); criteriaArgument = criterias.value(fieldName); if (!criteriaArgument.isValid()) {//try table.field criteriaArgument = criterias.value(tableName + "." + fieldName); } if (criteriaArgument.isValid()) {//criteria expression is just a parent of argument criteriaExpr = criteriaArgument.parent().toBinary(); usedCriterias.insert(criteriaArgument.toString(0).toString()); //save info. about used criteria } } } //create new row data KDbRecordData *newRecord = createNewRow(tableName, fieldName, true /* visible*/); if (criteriaExpr.isValid()) { //! @todo fix for !INFIX operators if (criteriaExpr.token() == '=') criteriaString = criteriaArgument.toString(0).toString(); else criteriaString = criteriaExpr.token().toString() + " " + criteriaArgument.toString(0).toString(); (*newRecord)[COLUMN_ID_CRITERIA] = criteriaString; } d->dataTable->dataAwareObject()->insertItem(newRecord, row_num); //OK, row inserted: create a new set for it KPropertySet &set = *createPropertySet(row_num, tableName, fieldName, true/*new one*/); if (!columnAlias.isEmpty()) set["alias"].setValue(columnAlias, false); if (!criteriaString.isEmpty()) set["criteria"].setValue(criteriaString, false); if (field->isExpression()) { if (!d->changeSingleCellValue(newRecord, COLUMN_ID_COLUMN, QVariant(columnAlias + ": " + field->expression().toString(0).toString()), &result)) return; //problems with setting column expression } row_num++; } //4. show ORDER BY information d->data->clearRecordEditBuffer(); const KDbOrderByColumnList* orderByColumns = query->orderByColumnList(); QHash columnsOrder( query->columnsOrder(KDbQuerySchema::UnexpandedListWithoutAsterisks)); for (KDbOrderByColumn::ListConstIterator orderByColumnIt(orderByColumns->constBegin()); orderByColumnIt != orderByColumns->constEnd(); ++orderByColumnIt) { KDbOrderByColumn* orderByColumn = *orderByColumnIt; KDbQueryColumnInfo *column = orderByColumn->column(); KDbRecordData *data = 0; KPropertySet *rowPropertySet = 0; if (column) { //sorting for visible column if (column->visible) { if (columnsOrder.contains(column)) { const int columnPosition = columnsOrder.value(column); data = d->data->at(columnPosition); rowPropertySet = d->sets->at(columnPosition); qDebug() << "\tSetting \"" << *orderByColumn << "\" sorting for record #" << columnPosition; } } } else if (orderByColumn->field()) { //this will be presented as invisible field: create new row KDbField* field = orderByColumn->field(); QString tableName(field->table() ? field->table()->name() : QString()); data = createNewRow(tableName, field->name(), false /* !visible*/); d->dataTable->dataAwareObject()->insertItem(data, row_num); rowPropertySet = createPropertySet(row_num, tableName, field->name(), true /*newOne*/); propertySetSwitched(); qDebug() << "\tSetting \"" << *orderByColumn << "\" sorting for invisible field" << field->name() << ", table " << tableName << " -row #" << row_num; row_num++; } //alter sorting for either existing or new row if (data && rowPropertySet) { // this will automatically update "sorting" property d->data->updateRecordEditBuffer(data, COLUMN_ID_SORTING, orderByColumn->ascending() ? 1 : 2); // in slotBeforeCellChanged() d->data->saveRecordChanges(data, true); (*rowPropertySet)["sorting"].clearModifiedFlag(); // this property should look "fresh" if (!(*data)[COLUMN_ID_VISIBLE].toBool()) //update (*rowPropertySet)["visible"].setValue(QVariant(false), false/*rememberOldValue*/); } } //5. Show fields for unused criterias (with "Visible" column set to false) foreach(const KDbExpression &criteriaArgument, criterias) { // <-- contains field or table.field if (usedCriterias.contains(criteriaArgument.toString(0).toString())) continue; //unused: append a new row KDbBinaryExpression criteriaExpr = criteriaArgument.parent().toBinary(); if (!criteriaExpr.isValid()) { qWarning() << "criteriaExpr is not a binary expr"; continue; } KDbVariableExpression columnNameArgument = criteriaExpr.left().toVariable(); //left or right if (!columnNameArgument.isValid()) { columnNameArgument = criteriaExpr.right().toVariable(); if (!columnNameArgument.isValid()) { qWarning() << "columnNameArgument is not a variable (table or table.field) expr"; continue; } } KDbField* field = 0; if (!columnNameArgument.name().contains('.') && query->tables()->count() == 1) { //extreme case: only field name provided for one-table query: field = query->tables()->first()->field(columnNameArgument.name()); } else { field = query->findTableField(columnNameArgument.name()); } if (!field) { qWarning() << "no columnInfo found in the query for name" << columnNameArgument.name(); continue; } QString tableName, fieldName, columnAlias, criteriaString; //! @todo what about ALIAS? tableName = field->table()->name(); fieldName = field->name(); //create new row data KDbRecordData *newRecord = createNewRow(tableName, fieldName, false /* !visible*/); if (criteriaExpr.isValid()) { //! @todo fix for !INFIX operators if (criteriaExpr.token() == '=') criteriaString = criteriaArgument.toString(0).toString(); else criteriaString = criteriaExpr.token().toString() + " " + criteriaArgument.toString(0).toString(); (*newRecord)[COLUMN_ID_CRITERIA] = criteriaString; } d->dataTable->dataAwareObject()->insertItem(newRecord, row_num); //OK, row inserted: create a new set for it KPropertySet &set = *createPropertySet(row_num++, tableName, fieldName, true/*new one*/); //! @todo if (!columnAlias.isEmpty()) //! @todo set["alias"].setValue(columnAlias, false); //// if (!criteriaString.isEmpty()) set["criteria"].setValue(criteriaString, false); set["visible"].setValue(QVariant(false), false); } //current property set has most probably changed propertySetSwitched(); if (!was_dirty) setDirty(false); //move to 1st column, 1st row d->dataTable->dataAwareObject()->ensureCellVisible(0, 0); // tempData()->registerTableSchemaChanges(query); } bool KexiQueryDesignerGuiEditor::loadLayout() { QString xml; //! @todo errmsg if (!loadDataBlock(&xml, "query_layout") || xml.isEmpty()) { //in a case when query layout was not saved, build layout by hand // -- dynamic cast because of a need for handling invalid queries // (as in KexiQueryDesignerGuiEditor::afterSwitchFrom()): KDbQuerySchema * q = dynamic_cast(window()->schemaObject()); if (q) { showTablesForQuery(q); KDbResultInfo result; showRelationsForQuery(q, result); if (!result.success) { window()->setStatus(&result, xi18n("Query definition loading failed.")); return false; } } return true; } QDomDocument doc; doc.setContent(xml); QDomElement doc_el = doc.documentElement(), el; if (doc_el.tagName() != "query_layout") { //! @todo errmsg return false; } const bool was_dirty = isDirty(); //add tables and relations to the relation view for (el = doc_el.firstChild().toElement(); !el.isNull(); el = el.nextSibling().toElement()) { if (el.tagName() == "table") { KDbTableSchema *t = d->conn->tableSchema(el.attribute("name")); int x = el.attribute("x", "-1").toInt(); int y = el.attribute("y", "-1").toInt(); int width = el.attribute("width", "-1").toInt(); int height = el.attribute("height", "-1").toInt(); QRect rect; if (x != -1 || y != -1 || width != -1 || height != -1) rect = QRect(x, y, width, height); d->relations->addTable(t, rect); } else if (el.tagName() == "conn") { SourceConnection src_conn; src_conn.masterTable = el.attribute("mtable"); src_conn.masterField = el.attribute("mfield"); src_conn.detailsTable = el.attribute("dtable"); src_conn.detailsField = el.attribute("dfield"); d->relations->addConnection(src_conn); } } if (!was_dirty) setDirty(false); return true; } bool KexiQueryDesignerGuiEditor::storeLayout() { KexiQueryPart::TempData * temp = tempData(); // Save SQL without driver-escaped keywords. if (window()->schemaObject()) //set this instance as obsolete (only if it's stored) d->conn->setQuerySchemaObsolete(window()->schemaObject()->name()); KDbSelectStatementOptions options; options.addVisibleLookupColumns = false; KDbNativeStatementBuilder builder; KDbEscapedString sql; if (!builder.generateSelectStatement(&sql, temp->query(), options)) { return false; } if (!storeDataBlock(sql.toString(), "sql")) { return false; } //serialize detailed XML query definition QString xml = "", tmp; foreach(KexiRelationsTableContainer* cont, *d->relations->tables()) { /*! @todo what about query? */ tmp = QString("schema()->name()) + "\" x=\"" + QString::number(cont->x()) + "\" y=\"" + QString::number(cont->y()) + "\" width=\"" + QString::number(cont->width()) + "\" height=\"" + QString::number(cont->height()) + "\"/>"; xml += tmp; } foreach(KexiRelationsConnection *conn, *d->relations->relationsConnections()) { tmp = QString("masterTable()->schema()->name()) + "\" mfield=\"" + conn->masterField() + "\" dtable=\"" + QString(conn->detailsTable()->schema()->name()) + "\" dfield=\"" + conn->detailsField() + "\"/>"; xml += tmp; } xml += ""; if (!storeDataBlock(xml, "query_layout")) { return false; } return true; } QSize KexiQueryDesignerGuiEditor::sizeHint() const { QSize s1 = d->relations->sizeHint(); QSize s2 = d->head->sizeHint(); return QSize(qMax(s1.width(), s2.width()), s1.height() + s2.height()); } KDbRecordData* KexiQueryDesignerGuiEditor::createNewRow(const QString& tableName, const QString& fieldName, bool visible) const { KDbRecordData *newRecord = d->data->createItem(); QString key; if (tableName == "*") key = "*"; else { if (!tableName.isEmpty()) key = (tableName + "."); key += fieldName; } (*newRecord)[COLUMN_ID_COLUMN] = key; (*newRecord)[COLUMN_ID_TABLE] = tableName; (*newRecord)[COLUMN_ID_VISIBLE] = QVariant(visible); #ifndef KEXI_NO_QUERY_TOTALS (*newRecord)[COLUMN_ID_TOTALS] = QVariant(0); #endif return newRecord; } void KexiQueryDesignerGuiEditor::slotDragOverTableRecord( KDbRecordData * /*data*/, int /*record*/, QDragMoveEvent* e) { if (e->mimeData()->hasFormat("kexi/field")) { e->setAccepted(true); } } void KexiQueryDesignerGuiEditor::slotDroppedAtRecord(KDbRecordData * /*data*/, int /*record*/, QDropEvent *ev, KDbRecordData*& newRecord) { QString sourcePartClass; QString srcTable; QStringList srcFields; Q_UNUSED(ev); /*! @todo KEXI3 Port kexidragobjects.cpp if (!KexiFieldDrag::decode(ev, &sourcePartClass, &srcTable, &srcFields)) return; */ if (srcFields.count() != 1) { return; } //insert new row at specific place newRecord = createNewRow(srcTable, srcFields[0], true /* visible*/); d->droppedNewRecord = newRecord; d->droppedNewTable = srcTable; d->droppedNewField = srcFields[0]; //! @todo } void KexiQueryDesignerGuiEditor::slotNewItemAppendedForAfterDeletingInSpreadSheetMode() { KDbRecordData *data = d->data->last(); if (data) (*data)[COLUMN_ID_VISIBLE] = QVariant(false); //the same init as in initTableRows() } void KexiQueryDesignerGuiEditor::slotRecordInserted(KDbRecordData* data, int record, bool /*repaint*/) { if (d->droppedNewRecord && d->droppedNewRecord == data) { createPropertySet(record, d->droppedNewTable, d->droppedNewField, true); propertySetSwitched(); d->droppedNewRecord = 0; } tempData()->setQueryChangedInView(true); } void KexiQueryDesignerGuiEditor::slotTableAdded(KDbTableSchema* /*t*/) { if (!d->slotTableAdded_enabled) return; updateColumnsData(); setDirty(); tempData()->setQueryChangedInView(true); d->dataTable->setFocus(); } void KexiQueryDesignerGuiEditor::slotTableHidden(KDbTableSchema* /*t*/) { updateColumnsData(); setDirty(); tempData()->setQueryChangedInView(true); } QByteArray KexiQueryDesignerGuiEditor::generateUniqueAlias() const { //! @todo add option for using non-i18n'd "expr" prefix? const QByteArray expStr( xi18nc("short for 'expression' word (only latin letters, please)", "expr").toLatin1()); //! @todo optimization: cache it? QSet aliases; const int setsSize = d->sets->size(); for (int r = 0; r < setsSize; r++) { //! @todo use iterator here KPropertySet *set = d->sets->at(r); if (set) { const QByteArray a((*set)["alias"].value().toByteArray().toLower()); if (!a.isEmpty()) aliases.insert(a); } } int aliasNr = 1; for (;;aliasNr++) { if (!aliases.contains(expStr + QByteArray::number(aliasNr))) break; } return expStr + QByteArray::number(aliasNr); } //! @todo this is primitive, temporary: reuse SQL parser KDbExpression KexiQueryDesignerGuiEditor::parseExpressionString(const QString& fullString, KDbToken *token, bool allowRelationalOperator) { Q_ASSERT(token); QString str = fullString.trimmed(); int len = 0; //KDbExpression expr; //1. get token *token = KDbToken(); //2-char-long tokens if (str.startsWith(QLatin1String(">="))) { *token = KDbToken::GREATER_OR_EQUAL; len = 2; } else if (str.startsWith(QLatin1String("<="))) { *token = KDbToken::LESS_OR_EQUAL; len = 2; } else if (str.startsWith(QLatin1String("<>"))) { *token = KDbToken::NOT_EQUAL; len = 2; } else if (str.startsWith(QLatin1String("!="))) { *token = KDbToken::NOT_EQUAL2; len = 2; } else if (str.startsWith(QLatin1String("=="))) { *token = '='; len = 2; } else if (str.startsWith(QLatin1String("LIKE "), Qt::CaseInsensitive)) { *token = KDbToken::LIKE; len = 5; } else if (str.startsWith(QLatin1String("NOT "), Qt::CaseInsensitive)) { str = str.mid(4).trimmed(); if (str.startsWith(QLatin1String("LIKE "), Qt::CaseInsensitive)) { *token = KDbToken::NOT_LIKE; len = 5; } else { return KDbExpression(); } } else { if (str.startsWith(QLatin1Char('=')) //1-char-long tokens || str.startsWith(QLatin1Char('<')) || str.startsWith(QLatin1Char('>'))) { *token = str[0].toLatin1(); len = 1; } else { if (allowRelationalOperator) *token = '='; } } if (!allowRelationalOperator && token->isValid()) return KDbExpression(); //1. get expression after token if (len > 0) str = str.mid(len).trimmed(); if (str.isEmpty()) return KDbExpression(); KDbExpression valueExpr; - QRegExp re; + QRegularExpressionMatch match; if (str.length() >= 2 && ( (str.startsWith(QLatin1Char('"')) && str.endsWith(QLatin1Char('"'))) || (str.startsWith(QLatin1Char('\'')) && str.endsWith(QLatin1Char('\'')))) ) { valueExpr = KDbConstExpression(KDbToken::CHARACTER_STRING_LITERAL, str.mid(1, str.length() - 2)); } else if (str.startsWith(QLatin1Char('[')) && str.endsWith(QLatin1Char(']'))) { valueExpr = KDbQueryParameterExpression(str.mid(1, str.length() - 2)); - } else if ((re = QRegExp("(\\d{1,4})-(\\d{1,2})-(\\d{1,2})")).exactMatch(str)) { + } else if ((match = QRegularExpression("^(\\d{1,4})-(\\d{1,2})-(\\d{1,2})$").match(str)).hasMatch()) { valueExpr = KDbConstExpression(KDbToken::DATE_CONST, QDate::fromString( - re.cap(1).rightJustified(4, '0') + "-" + re.cap(2).rightJustified(2, '0') - + "-" + re.cap(3).rightJustified(2, '0'), Qt::ISODate)); - } else if ((re = QRegExp("(\\d{1,2}):(\\d{1,2})")).exactMatch(str) - || (re = QRegExp("(\\d{1,2}):(\\d{1,2}):(\\d{1,2})")).exactMatch(str)) { - QString res = re.cap(1).rightJustified(2, '0') + ":" + re.cap(2).rightJustified(2, '0') - + ":" + re.cap(3).rightJustified(2, '0'); + match.captured(1).rightJustified(4, '0') + "-" + match.captured(2).rightJustified(2, '0') + + "-" + match.captured(3).rightJustified(2, '0'), Qt::ISODate)); + } else if ((match = QRegularExpression("^(\\d{1,2}):(\\d{1,2})$").match(str)).hasMatch() + || (match = QRegularExpression("^(\\d{1,2}):(\\d{1,2}):(\\d{1,2})$").match(str)).hasMatch()) { + QString res = match.captured(1).rightJustified(2, '0') + ":" + match.captured(2).rightJustified(2, '0') + + ":" + match.captured(3).rightJustified(2, '0'); // qDebug() << res; valueExpr = KDbConstExpression(KDbToken::TIME_CONST, QTime::fromString(res, Qt::ISODate)); - } else if ((re = QRegExp("(\\d{1,4})-(\\d{1,2})-(\\d{1,2})\\s+(\\d{1,2}):(\\d{1,2})")).exactMatch(str) - || (re = QRegExp("(\\d{1,4})-(\\d{1,2})-(\\d{1,2})\\s+(\\d{1,2}):(\\d{1,2}):(\\d{1,2})")).exactMatch(str)) { - QString res = re.cap(1).rightJustified(4, '0') + "-" + re.cap(2).rightJustified(2, '0') - + "-" + re.cap(3).rightJustified(2, '0') - + "T" + re.cap(4).rightJustified(2, '0') + ":" + re.cap(5).rightJustified(2, '0') - + ":" + re.cap(6).rightJustified(2, '0'); + } else if ((match = QRegularExpression("^(\\d{1,4})-(\\d{1,2})-(\\d{1,2})\\s+(\\d{1,2}):(\\d{1,2})$").match(str)).hasMatch() + || (match = QRegularExpression("^(\\d{1,4})-(\\d{1,2})-(\\d{1,2})\\s+(\\d{1,2}):(\\d{1,2}):(\\d{1,2})$").match(str)).hasMatch()) { + QString res = match.captured(1).rightJustified(4, '0') + "-" + match.captured(2).rightJustified(2, '0') + + "-" + match.captured(3).rightJustified(2, '0') + + "T" + match.captured(4).rightJustified(2, '0') + ":" + match.captured(5).rightJustified(2, '0') + + ":" + match.captured(6).rightJustified(2, '0'); // qDebug() << res; valueExpr = KDbConstExpression(KDbToken::DATETIME_CONST, QDateTime::fromString(res, Qt::ISODate)); } else if ((str[0] >= '0' && str[0] <= '9') || str[0] == '-' || str[0] == '+') { //number QLocale locale; const QChar decimalSym = locale.decimalPoint(); bool ok; int pos = str.indexOf('.'); if (pos == -1) {//second chance: local decimal symbol pos = str.indexOf(decimalSym); } if (pos >= 0) {//real const number const int left = str.left(pos).toInt(&ok); if (!ok) return KDbExpression(); const int right = str.mid(pos + 1).toInt(&ok); if (!ok) return KDbExpression(); valueExpr = KDbConstExpression(KDbToken::REAL_CONST, QPoint(left, right)); //decoded to QPoint } else { //integer const const qint64 val = str.toLongLong(&ok); if (!ok) return KDbExpression(); valueExpr = KDbConstExpression(KDbToken::INTEGER_CONST, val); } } else if (str.toLower() == "null") { valueExpr = KDbConstExpression(KDbToken::SQL_NULL, QVariant()); } else {//identfier if (!KDb::isIdentifier(str)) return KDbExpression(); valueExpr = KDbVariableExpression(str); // the default is 'fieldname' //find first matching field for name 'str': foreach(KexiRelationsTableContainer *cont, *d->relations->tables()) { /*! @todo what about query? */ if (cont->schema()->table() && cont->schema()->table()->field(str)) { valueExpr = KDbVariableExpression(cont->schema()->table()->name() + '.' + str); // the expression is now: tablename.fieldname //! @todo KEXI3 check this we're calling KDbQuerySchema::validate() instead of this: valueExpr.toVariable().field = cont->schema()->table()->field(str); break; } } } return valueExpr; } void KexiQueryDesignerGuiEditor::slotBeforeCellChanged(KDbRecordData *data, int colnum, QVariant* newValue, KDbResultInfo* result) { switch (colnum) { case COLUMN_ID_COLUMN: slotBeforeColumnCellChanged(data, *newValue, result); break; case COLUMN_ID_TABLE: slotBeforeTableCellChanged(data, *newValue, result); break; case COLUMN_ID_VISIBLE: slotBeforeVisibleCellChanged(data, *newValue, result); break; #ifndef KEXI_NO_QUERY_TOTALS case COLUMN_ID_TOTALS: slotBeforeTotalsCellChanged(data, newValue, result); break; #endif case COLUMN_ID_SORTING: slotBeforeSortingCellChanged(data, *newValue, result); break; case COLUMN_ID_CRITERIA: slotBeforeCriteriaCellChanged(data, *newValue, result); break; default: Q_ASSERT_X(false, "colnum", "unhandled value"); } } void KexiQueryDesignerGuiEditor::slotBeforeColumnCellChanged(KDbRecordData *data, QVariant& newValue, KDbResultInfo* result) { if (newValue.isNull()) { d->data->updateRecordEditBuffer(data, COLUMN_ID_TABLE, QVariant(), false/*!allowSignals*/); d->data->updateRecordEditBuffer(data, COLUMN_ID_VISIBLE, QVariant(false));//invisible d->data->updateRecordEditBuffer(data, COLUMN_ID_SORTING, QVariant()); #ifndef KEXI_NO_QUERY_TOTALS d->data->updateRecordEditBuffer(data, COLUMN_ID_TOTALS, QVariant());//remove totals #endif d->data->updateRecordEditBuffer(data, COLUMN_ID_CRITERIA, QVariant());//remove crit. d->sets->eraseCurrentPropertySet(); return; } //auto fill 'table' column QString fieldId(newValue.toString().trimmed()); //tmp, can look like "table.field" QString fieldName; //"field" part of "table.field" or expression string QString tableName; //empty for expressions QByteArray alias; QString columnValueForExpr; //for setting pretty printed "alias: expr" in 1st column const bool isExpression = !d->fieldColumnIdentifiers.contains(fieldId.toLower()); if (isExpression) { //this value is entered by hand and doesn't match //any value in the combo box -- we're assuming this is an expression //-table remains null //-find "alias" in something like "alias : expr" const int id = fieldId.indexOf(':'); if (id > 0) { alias = fieldId.left(id).trimmed().toLatin1(); if (!KDb::isIdentifier(alias)) { result->success = false; result->allowToDiscardChanges = true; result->column = COLUMN_ID_COLUMN; result->msg = xi18nc("@info", "Entered column alias %1 is not a valid identifier.", QString::fromLatin1(alias)); result->desc = xi18n("Identifiers should start with a letter or '_' character"); return; } } fieldName = fieldId.mid(id + 1).trimmed(); //check expr. KDbExpression e; KDbToken dummyToken; if ((e = parseExpressionString(fieldName, &dummyToken, false/*allowRelationalOperator*/)).isValid()) { fieldName = e.toString(0).toString(); //print it prettier //this is just checking: destroy expr. object } else { result->success = false; result->allowToDiscardChanges = true; result->column = COLUMN_ID_COLUMN; result->msg = xi18nc("@info", "Invalid expression %1", fieldName); return; } } else {//not expr. //this value is properly selected from combo box list if (fieldId == "*") { tableName = "*"; } else { if (!KDb::splitToTableAndFieldParts( fieldId, &tableName, &fieldName, KDb::SetFieldNameIfNoTableName)) { qWarning() << "no 'field' or 'table.field'"; return; } } } bool saveOldValue = true; KPropertySet *set = d->sets->findPropertySetForItem(*data); if (!set) { saveOldValue = false; // no old val. const int row = d->data->indexOf(data); if (row < 0) { result->success = false; return; } set = createPropertySet(row, tableName, fieldName, true); propertySetSwitched(); } d->data->updateRecordEditBuffer(data, COLUMN_ID_TABLE, QVariant(tableName), false/*!allowSignals*/); d->data->updateRecordEditBuffer(data, COLUMN_ID_VISIBLE, QVariant(true)); #ifndef KEXI_NO_QUERY_TOTALS d->data->updateRecordEditBuffer(data, COLUMN_ID_TOTALS, QVariant(0)); #endif if (!sortingAllowed(fieldName, tableName)) { // sorting is not available for "*" or "table.*" rows //! @todo what about expressions? d->data->updateRecordEditBuffer(data, COLUMN_ID_SORTING, QVariant()); } //update properties (*set)["field"].setValue(fieldName, saveOldValue); if (isExpression) { //-no alias but it's needed: if (alias.isEmpty()) //-try oto get old alias alias = (*set)["alias"].value().toByteArray(); if (alias.isEmpty()) //-generate smallest unique alias alias = generateUniqueAlias(); } (*set)["isExpression"].setValue(QVariant(isExpression), saveOldValue); if (!alias.isEmpty()) { (*set)["alias"].setValue(alias, saveOldValue); //pretty printed "alias: expr" newValue = QString(QString(alias) + ": " + fieldName); } (*set)["caption"].setValue(QString(), saveOldValue); (*set)["table"].setValue(tableName, saveOldValue); updatePropertiesVisibility(*set); } void KexiQueryDesignerGuiEditor::slotBeforeTableCellChanged(KDbRecordData *data, QVariant& newValue, KDbResultInfo* result) { Q_UNUSED(result) if (newValue.isNull()) { if (!(*data)[COLUMN_ID_COLUMN].toString().isEmpty()) { d->data->updateRecordEditBuffer(data, COLUMN_ID_COLUMN, QVariant(), false/*!allowSignals*/); } d->data->updateRecordEditBuffer(data, COLUMN_ID_VISIBLE, QVariant(false));//invisible #ifndef KEXI_NO_QUERY_TOTALS d->data->updateRecordEditBuffer(data, COLUMN_ID_TOTALS, QVariant());//remove totals #endif d->data->updateRecordEditBuffer(data, COLUMN_ID_CRITERIA, QVariant());//remove crit. d->sets->eraseCurrentPropertySet(); } //update property KPropertySet *set = d->sets->findPropertySetForItem(*data); if (set) { if ((*set)["isExpression"].value().toBool() == false) { (*set)["table"] = newValue; (*set)["caption"] = QVariant(QString()); } else { //do not set table for expr. columns newValue = QVariant(); } updatePropertiesVisibility(*set); } } void KexiQueryDesignerGuiEditor::slotBeforeVisibleCellChanged(KDbRecordData *data, QVariant& newValue, KDbResultInfo* result) { Q_UNUSED(result) bool saveOldValue = true; if (!propertySet()) { saveOldValue = false; createPropertySet(d->dataTable->dataAwareObject()->currentRecord(), (*data)[COLUMN_ID_TABLE].toString(), (*data)[COLUMN_ID_COLUMN].toString(), true); #ifndef KEXI_NO_QUERY_TOTALS d->data->updateRecordEditBuffer(data, COLUMN_ID_TOTALS, QVariant(0));//totals #endif propertySetSwitched(); } KPropertySet &set = *propertySet(); set["visible"].setValue(newValue, saveOldValue); } void KexiQueryDesignerGuiEditor::slotBeforeTotalsCellChanged(KDbRecordData *data, QVariant& newValue, KDbResultInfo* result) { #ifdef KEXI_NO_QUERY_TOTALS Q_UNUSED(data) Q_UNUSED(newValue) Q_UNUSED(result) #else //! @todo unused yet setDirty(true); tempData()->setQueryChangedInView(true); #endif } void KexiQueryDesignerGuiEditor::slotBeforeSortingCellChanged(KDbRecordData *data, QVariant& newValue, KDbResultInfo* result) { bool saveOldValue = true; KPropertySet *set = d->sets->findPropertySetForItem(*data); if (!set) { saveOldValue = false; set = createPropertySet(d->dataTable->dataAwareObject()->currentRecord(), (*data)[COLUMN_ID_TABLE].toString(), (*data)[COLUMN_ID_COLUMN].toString(), true); #ifndef KEXI_NO_QUERY_TOTALS d->data->updateRecordEditBuffer(data, COLUMN_ID_TOTALS, QVariant(0));//totals #endif propertySetSwitched(); } QString table(set->property("table").value().toString()); QString field(set->property("field").value().toString()); if (newValue.toInt() == 0 || sortingAllowed(field, table)) { KProperty &property = set->property("sorting"); QString key(property.listData()->keysAsStringList()[ newValue.toInt()]); qDebug() << "new key=" << key; property.setValue(key, saveOldValue); } else { //show msg: sorting is not available result->success = false; result->allowToDiscardChanges = true; result->column = COLUMN_ID_SORTING; result->msg = xi18n("Could not set sorting for multiple columns (%1)", table == "*" ? table : (table + ".*")); } } void KexiQueryDesignerGuiEditor::slotBeforeCriteriaCellChanged(KDbRecordData *data, QVariant& newValue, KDbResultInfo* result) { //! @todo this is primitive, temporary: reuse SQL parser //QString operatorStr, argStr; KDbExpression e; const QString str = newValue.toString().trimmed(); KDbToken token; QString field, table; KPropertySet *set = d->sets->findPropertySetForItem(*data); if (set) { field = (*set)["field"].value().toString(); table = (*set)["table"].value().toString(); } if (!str.isEmpty() && (!set || table == "*" || field.contains("*"))) { //asterisk found! criteria not allowed result->success = false; result->allowToDiscardChanges = true; result->column = COLUMN_ID_CRITERIA; if (propertySet()) result->msg = xi18nc("@info", "Could not set criteria for %1", table == "*" ? table : field); else result->msg = xi18n("Could not set criteria for empty record"); } else if (str.isEmpty() || (e = parseExpressionString(str, &token, true/*allowRelationalOperator*/)).isValid()) { if (e.isValid()) { QString tokenStr; if (token != '=') { tokenStr = token.toString() + " "; } if (set) { (*set)["criteria"] = QString(tokenStr + e.toString(0).toString()); //print it prettier } //this is just checking: destroy expr. object } else if (set && str.isEmpty()) { (*set)["criteria"] = QVariant(); //clear it } setDirty(true); tempData()->setQueryChangedInView(true); } else { result->success = false; result->allowToDiscardChanges = true; result->column = COLUMN_ID_CRITERIA; result->msg = xi18nc("@info", "Invalid criteria %1", newValue.toString()); } } void KexiQueryDesignerGuiEditor::slotTablePositionChanged(KexiRelationsTableContainer*) { setDirty(true); // this is not needed here because only position has changed: tempData()->setQueryChangedInView(true); } void KexiQueryDesignerGuiEditor::slotAboutConnectionRemove(KexiRelationsConnection*) { setDirty(true); tempData()->setQueryChangedInView(true); } void KexiQueryDesignerGuiEditor::slotAppendFields( KDbTableOrQuerySchema& tableOrQuery, const QStringList& fieldNames) { //! @todo how about query columns and multiple fields? KDbTableSchema *table = tableOrQuery.table(); if (!table || fieldNames.isEmpty()) return; QString fieldName(fieldNames.first()); if (fieldName != "*" && !table->field(fieldName)) return; int row_num; //find last filled row in the GUI table for (row_num = d->sets->size() - 1; row_num >= 0 && !d->sets->at(row_num); row_num--) { } row_num++; //after //add row KDbRecordData *newRecord = createNewRow(table->name(), fieldName, true /* visible*/); d->dataTable->dataAwareObject()->insertItem(newRecord, row_num); d->dataTable->dataAwareObject()->setCursorPosition(row_num, 0); //create buffer createPropertySet(row_num, table->name(), fieldName, true/*new one*/); propertySetSwitched(); d->dataTable->setFocus(); } KPropertySet *KexiQueryDesignerGuiEditor::propertySet() { return d->sets->currentPropertySet(); } void KexiQueryDesignerGuiEditor::updatePropertiesVisibility(KPropertySet& set) { const bool asterisk = isAsterisk( set["table"].value().toString(), set["field"].value().toString() ); #ifdef KEXI_SHOW_UNFINISHED set["caption"].setVisible(!asterisk); #endif set["alias"].setVisible(!asterisk); /*always invisible #ifdef KEXI_SHOW_UNFINISHED set["sorting"].setVisible( !asterisk ); #endif*/ propertySetReloaded(true); } KPropertySet* KexiQueryDesignerGuiEditor::createPropertySet(int row, const QString& tableName, const QString& fieldName, bool newOne) { //const bool asterisk = isAsterisk(tableName, fieldName); KPropertySet *set = new KPropertySet(d->sets); KProperty *prop; //meta-info for property editor set->addProperty(prop = new KProperty("this:classString", xi18nc("Query column", "Column"))); prop->setVisible(false); //! \todo add table_field icon (add buff->addProperty(prop = new KexiProperty("this:iconName", KexiIconName("table_field")) ); // prop->setVisible(false); set->addProperty(prop = new KProperty("this:visibleObjectNameProperty", "visibleName")); prop->setVisible(false);//always hidden set->addProperty(prop = new KProperty("this:objectNameReadOnly", true)); prop->setVisible(false);//always hidden set->addProperty(prop = new KProperty("visibleName", QVariant(tableName + '.' + fieldName))); prop->setVisible(false);//always hidden set->addProperty(prop = new KProperty("table", QVariant(tableName))); prop->setVisible(false);//always hidden set->addProperty(prop = new KProperty("field", QVariant(fieldName))); prop->setVisible(false);//always hidden set->addProperty(prop = new KProperty("caption", QVariant(QString()), xi18n("Caption"))); #ifndef KEXI_SHOW_UNFINISHED prop->setVisible(false); #endif set->addProperty(prop = new KProperty("alias", QVariant(QString()), xi18n("Alias"))); set->addProperty(prop = new KProperty("visible", QVariant(true))); prop->setVisible(false); /*! @todo set->addProperty(prop = new KexiProperty("totals", QVariant(QString())) ); prop->setVisible(false);*/ //sorting QStringList slist, nlist; slist << "nosorting" << "ascending" << "descending"; nlist << xi18n("None") << xi18n("Ascending") << xi18n("Descending"); set->addProperty(prop = new KProperty("sorting", slist, nlist, slist[0], xi18n("Sorting"))); prop->setVisible(false); set->addProperty(prop = new KProperty("criteria", QVariant(QString()))); prop->setVisible(false); set->addProperty(prop = new KProperty("isExpression", QVariant(false))); prop->setVisible(false); d->sets->set(row, set, newOne); updatePropertiesVisibility(*set); return set; } void KexiQueryDesignerGuiEditor::setFocus() { d->dataTable->setFocus(); } void KexiQueryDesignerGuiEditor::slotPropertyChanged(KPropertySet& set, KProperty& property) { const QByteArray pname(property.name()); /*! @todo use KexiProperty::setValidator(QString) when implemented as described in TODO #60 */ if (pname == "alias" || pname == "name") { const QVariant& v = property.value(); if (!v.toString().trimmed().isEmpty() && !KDb::isIdentifier(v.toString())) { KMessageBox::sorry(this, KDb::identifierExpectedMessage(property.caption(), v.toString())); property.resetValue(); } if (pname == "alias") { if (set["isExpression"].value().toBool() == true) { //update value in column #1 d->dataTable->dataAwareObject()->acceptEditor(); d->data->updateRecordEditBuffer(d->dataTable->dataAwareObject()->selectedRecord(), 0, QVariant(set["alias"].value().toString() + ": " + set["field"].value().toString())); d->data->saveRecordChanges(d->dataTable->dataAwareObject()->selectedRecord(), true); } } } tempData()->setQueryChangedInView(true); } void KexiQueryDesignerGuiEditor::slotNewItemStored(KexiPart::Item* item) { d->relations->objectCreated(item->pluginId(), item->name()); } void KexiQueryDesignerGuiEditor::slotItemRemoved(const KexiPart::Item& item) { d->relations->objectDeleted(item.pluginId(), item.name()); } void KexiQueryDesignerGuiEditor::slotItemRenamed(const KexiPart::Item& item, const QString& oldName) { d->relations->objectRenamed(item.pluginId(), oldName, item.name()); } diff --git a/src/plugins/scripting/kexidb/kexidbschema.cpp b/src/plugins/scripting/kexidb/kexidbschema.cpp index 7b29c313d..28552582e 100644 --- a/src/plugins/scripting/kexidb/kexidbschema.cpp +++ b/src/plugins/scripting/kexidb/kexidbschema.cpp @@ -1,176 +1,176 @@ /*************************************************************************** * kexidbschema.cpp * This file is part of the KDE project * copyright (C)2004-2006 by Sebastian Sauer (mail@dipe.org) * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * You should have received a copy of the GNU Library General Public License * along with this program; see the file COPYING. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. ***************************************************************************/ #include "kexidbschema.h" #include "kexidbfieldlist.h" -#include +#include #include using namespace Scripting; /*************************************************************************** *KexiDBSchema */ KexiDBSchema::KexiDBSchema(QObject* parent, const QString& name, KDbObject* schema, KDbFieldList* fieldlist, bool owner) : QObject(parent) , m_schema(schema) , m_fieldlist(fieldlist) , m_owner(owner) { setObjectName(name); } KexiDBSchema::~KexiDBSchema() { } const QString KexiDBSchema::name() const { return m_schema->name(); } void KexiDBSchema::setName(const QString& name) { m_schema->setName(name); } const QString KexiDBSchema::caption() const { return m_schema->caption(); } void KexiDBSchema::setCaption(const QString& caption) { m_schema->setCaption(caption); } const QString KexiDBSchema::description() const { return m_schema->description(); } void KexiDBSchema::setDescription(const QString& description) { m_schema->setDescription(description); } QObject* KexiDBSchema::fieldlist() { return new KexiDBFieldList(this, m_fieldlist, false); } /*************************************************************************** * KexiDBTableSchema */ KexiDBTableSchema::KexiDBTableSchema(QObject* parent, KDbTableSchema* tableschema, bool owner) : KexiDBSchema(parent, "KexiDBTableSchema", tableschema, tableschema, owner) { } KexiDBTableSchema::~KexiDBTableSchema() { if (m_owner) delete tableschema(); } KDbTableSchema* KexiDBTableSchema::tableschema() { return static_cast< KDbTableSchema* >(m_schema); } QObject* KexiDBTableSchema::query() { return new KexiDBQuerySchema(this, tableschema()->query(), false); } /*************************************************************************** * KexiDBQuerySchema */ KexiDBQuerySchema::KexiDBQuerySchema(QObject* parent, KDbQuerySchema* queryschema, bool owner) : KexiDBSchema(parent, "KexiDBQuerySchema", queryschema, queryschema, owner) { } KexiDBQuerySchema::~KexiDBQuerySchema() { if (m_owner) delete queryschema(); } KDbQuerySchema* KexiDBQuerySchema::queryschema() { return static_cast< KDbQuerySchema* >(m_schema); } const QString KexiDBQuerySchema::statement() const { return static_cast< KDbQuerySchema* >(m_schema)->statement(); } void KexiDBQuerySchema::setStatement(const QString& statement) { static_cast< KDbQuerySchema* >(m_schema)->setStatement(statement); } bool KexiDBQuerySchema::setWhereExpression(const QString& whereexpression) { KDbExpression* oldexpr = static_cast< KDbQuerySchema* >(m_schema)->whereExpression(); Q_UNUSED(oldexpr); ///@todo use KDbParser for such kind of parser-functionality. QString s = whereexpression; - QRegExp re("[\"',]{1,1}"); + QRegularExpression re("[\"',]{1,1}"); while (true) { - s.remove(QRegExp("^[\\s,]+")); + s.remove(QRegularExpression("^[\\s,]+")); int pos = s.indexOf('='); if (pos < 0) break; QString key = s.left(pos).trimmed(); s = s.mid(pos + 1).trimmed(); QString value; int sp = s.indexOf(re); if (sp >= 0) { if (re.cap(0) == ",") { value = s.left(sp).trimmed(); s = s.mid(sp + 1).trimmed(); } else { int ep = s.indexOf(re.cap(0), sp + 1); value = s.mid(sp + 1, ep - 1); s = s.mid(ep + 1); } } else { value = s; s.clear(); } KDbField* field = static_cast< KDbQuerySchema* >(m_schema)->field(key); if (! field) { qWarning() << QString("Invalid WHERE-expression: Field \"%1\" does not exists in tableschema \"%2\".").arg(key).arg(m_schema->name()); return false; } QVariant v(value); if (! v.convert(field->variantType())) { qWarning() << QString("Invalid WHERE-expression: The for Field \"%1\" defined value is of type \"%2\" rather then the expected type \"%3\"").arg(key).arg(v.typeName()).arg(field->variantType()); return false; } static_cast< KDbQuerySchema* >(m_schema)->addToWhereExpression(field, v); } return true; } diff --git a/src/tests/altertable/altertable.cpp b/src/tests/altertable/altertable.cpp index 433d325af..5b20e50b5 100644 --- a/src/tests/altertable/altertable.cpp +++ b/src/tests/altertable/altertable.cpp @@ -1,698 +1,698 @@ /* This file is part of the KDE project Copyright (C) 2006 Jarosław Staniek This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "altertable.h" #include
#include #include #include #include #include #include #include #include #include #include #include -#include +#include #include #include #include QString testFilename; QFile testFile; QTextStream testFileStream; QStringList testFileLine; int testLineNumber = 0; QString origDbFilename, dbFilename; int variableI = 1; // simple variable 'i' support int newArgc; char** newArgv; KexiMainWindow* win = 0; KexiProject* prj = 0; void showError(const QString& msg) { QString msg_(msg); msg_.prepend(QString("Error at line %1: ").arg(testLineNumber)); qDebug() << msg_; } /* Reads a single line from testFileStream, fills testFileLine, updates testLineNumber text in quotes is extracted, e.g. \"ab c\" is treat as one item "ab c" Returns flas on failure (e.g. end of file). Empty lines and lines or parts of lines with # (comments) are omitted. */ tristate readLineFromTestFile(const QString& expectedCommandName = QString()) { QString s; bool blockComment = false; while (true) { if (testFileStream.atEnd()) return cancelled; testLineNumber++; s = testFileStream.readLine().stripWhiteSpace(); if (blockComment) { if (s.endsWith("*/")) blockComment = false; continue; } if (!blockComment && s.startsWith("/*")) { blockComment = true; continue; } if (s.startsWith("#")) continue; //skip commented line if (!s.isEmpty()) break; } s.append(" "); //sentinel QString item; testFileLine.clear(); const int len = s.length(); bool skipWhiteSpace = true, quoted = false; for (int i = 0; i < len; i++) { const QChar ch(s.ref(i)); if (skipWhiteSpace) { if (ch == '#') break; //eoln if (ch == ' ' || ch == '\t') continue; skipWhiteSpace = false; if (ch == '\"') { quoted = true; continue; } item.append(ch); } else { if ((quoted && ch == '\"') || (!quoted && (ch == ' ' || ch == '\t'))) { //end of item skipWhiteSpace = true; quoted = false; testFileLine.append(item); item.clear(); continue; } item.append(ch); } } if (!expectedCommandName.isEmpty() && testFileLine[0] != expectedCommandName) { showError(QString("Invalid command '%1', expected '%2'") .arg(testFileLine[0]).arg(expectedCommandName)); return false; } if (quoted) { showError("Invalid contents"); return false; } return true; } bool checkItemsNumber(int expectedNumberOfItems, int optionalNumberOfItems = -1) { bool ok = expectedNumberOfItems == (int)testFileLine.count(); if (optionalNumberOfItems > 0) ok = ok || optionalNumberOfItems == (int)testFileLine.count(); if (!ok) { QString msg = QString("Invalid number of args (%1) for command '%2', expected: %3") .arg(testFileLine.count()).arg(testFileLine[0]).arg(expectedNumberOfItems); if (optionalNumberOfItems > 0) msg.append(QString(" or %1").arg(optionalNumberOfItems)); showError(msg); return false; } return true; } QVariant::Type typeNameToQVariantType(const QCString& name_) { QCString name(name_.toLower()); if (name == "string") return QVariant::String; if (name == "int") return QVariant::Int; if (name == "bool" || name == "boolean") return QVariant::Bool; if (name == "double" || name == "float") return QVariant::Double; if (name == "date") return QVariant::Date; if (name == "datetime") return QVariant::DateTime; if (name == "time") return QVariant::Time; if (name == "bytearray") return QVariant::ByteArray; if (name == "longlong") return QVariant::LongLong; //! @todo more types showError(QString("Invalid type '%1'").arg(name_)); return QVariant::Invalid; } // casts string to QVariant bool castStringToQVariant(const QString& string, const QCString& type, QVariant& result) { if (string.toLower() == "") { result = QVariant(); return true; } if (string == "\"\"") { result = QString(""); return true; } const QVariant::Type vtype = typeNameToQVariantType(type); bool ok; result = KDb::stringToVariant(string, vtype, &ok); return ok; } // returns a number parsed from argument; if argument is i or i++, variableI is used // 'ok' is set to false on failure static int getNumber(const QString& argument, bool *ok) { int result; *ok = true; if (argument == "i" || argument == "i++") { result = variableI; if (argument == "i++") variableI++; } else { result = argument.toInt(ok); if (!*ok) { showError(QString("Invalid value '%1'").arg(argument)); return -1; } } return result; } //--------------------------------------- AlterTableTester::AlterTableTester() : QObject() , m_finishedCopying(false) { //copy the db file to a temp file qInitNetworkProtocols(); QPtrList list = m_copyOperator.copy( "file://" + QDir::current().path() + "/" + origDbFilename, "file://" + QDir::current().path() + "/" + dbFilename, false, false); connect(&m_copyOperator, SIGNAL(finished(QNetworkOperation*)), this, SLOT(slotFinishedCopying(QNetworkOperation*))); } AlterTableTester::~AlterTableTester() { QFile(dbFilename).remove(); } void AlterTableTester::slotFinishedCopying(QNetworkOperation* oper) { if (oper->operation() == QNetworkProtocol::OpPut) m_finishedCopying = true; } bool AlterTableTester::changeFieldProperty(KexiTableDesignerInterface* designerIface) { if (!checkItemsNumber(5)) return false; QVariant newValue; QCString propertyName(testFileLine[2].toLatin1()); QCString propertyType(testFileLine[3].toLatin1()); QString propertyValueString(testFileLine[4]); if (propertyName == "type") newValue = (int)KDbField::typeForString(testFileLine[4]); else { if (!castStringToQVariant(propertyValueString, propertyType, newValue)) { showError(QString("Could not set property '%1' value '%2' of type '%3'") .arg(propertyName).arg(propertyValueString).arg(propertyType)); return false; } } bool ok; int row = getNumber(testFileLine[1], &ok) - 1; if (!ok) return false; designerIface->changeFieldPropertyForRow(row, propertyName, newValue, 0, true); if (propertyName == "type") { //clean subtype name, e.g. from "longText" to "LongText", because dropdown list is case-sensitive QString realSubTypeName; if (KDbField::BLOB == KDbField::typeForString(testFileLine[4])) //! @todo hardcoded! realSubTypeName = "image"; else realSubTypeName = KDbField::typeString(KDbField::typeForString(testFileLine[4])); designerIface->changeFieldPropertyForRow(row, "subType", realSubTypeName, 0, true); } return true; } //helper bool AlterTableTester::getSchemaDump(KexiWindow* window, QString& schemaDebugString) { KexiTableDesignerInterface* designerIface = dynamic_cast(window->selectedView()); if (!designerIface) return false; // Get the result tristate result; schemaDebugString = designerIface->debugStringForCurrentTableSchema(result); if (true != result) { showError(QString("Loading modified schema failed. Result: %1") .arg(~result ? "cancelled" : "false")); return false; } - schemaDebugString.remove(QRegExp(",$")); //no need to have "," at the end of lines + schemaDebugString.remove(QRegularExpression(",$")); //no need to have "," at the end of lines return true; } bool AlterTableTester::showSchema(KexiWindow* window, bool copyToClipboard) { QString schemaDebugString; if (!getSchemaDump(window, schemaDebugString)) return false; if (copyToClipboard) QApplication::clipboard()->setText(schemaDebugString); else qDebug() << QString("Schema for '%1' table:\n").arg(window->partItem()->name()) + schemaDebugString + "\nendSchema"; return true; } bool AlterTableTester::checkInternal(KexiWindow* window, QString& debugString, const QString& endCommand, bool skipColonsAndStripWhiteSpace) { Q_UNUSED(window); QTextStream resultStream(&debugString, IO_ReadOnly); // Load expected result, compare QString expectedLine, resultLine; while (true) { const bool testFileStreamAtEnd = testFileStream.atEnd(); if (!testFileStreamAtEnd) { testLineNumber++; expectedLine = testFileStream.readLine(); if (skipColonsAndStripWhiteSpace) { expectedLine = expectedLine.stripWhiteSpace(); - expectedLine.remove(QRegExp(",$")); //no need to have "," at the end of lines + expectedLine.remove(QRegularExpression(",$")); //no need to have "," at the end of lines } } if (testFileStreamAtEnd || endCommand == expectedLine.stripWhiteSpace()) { if (!resultStream.atEnd()) { showError("Test file ends unexpectedly."); return false; } break; } //test line loaded, load result if (resultStream.atEnd()) { showError(QString("Result ends unexpectedly. There is at least one additinal test line: '") + expectedLine + "'"); return false; } resultLine = resultStream.readLine(); if (skipColonsAndStripWhiteSpace) { resultLine = resultLine.stripWhiteSpace(); - resultLine.remove(QRegExp(",$")); //no need to have "," at the end of lines + resultLine.remove(QRegularExpressions(",$")); //no need to have "," at the end of lines } if (resultLine != expectedLine) { showError( QString("Result differs from the expected:\nExpected: ") + expectedLine + "\n????????: " + resultLine + "\n"); return false; } } return true; } bool AlterTableTester::checkSchema(KexiWindow* window) { QString schemaDebugString; if (!getSchemaDump(window, schemaDebugString)) return false; bool result = checkInternal(window, schemaDebugString, "endSchema", true /*skipColonsAndStripWhiteSpace*/); qDebug() << QString("Schema check for table '%1': %2").arg(window->partItem()->name()) .arg(result ? "OK" : "Failed"); return result; } bool AlterTableTester::getActionsDump(KexiWindow* window, QString& actionsDebugString) { KexiTableDesignerInterface* designerIface = dynamic_cast(window->selectedView()); if (!designerIface) return false; tristate result = designerIface->simulateAlterTableExecution(&actionsDebugString); if (true != result) { showError(QString("Computing simplified actions for table '%1' failed.").arg(window->partItem()->name())); return false; } return true; } bool AlterTableTester::showActions(KexiWindow* window, bool copyToClipboard) { QString actionsDebugString; if (!getActionsDump(window, actionsDebugString)) return false; if (copyToClipboard) QApplication::clipboard()->setText(actionsDebugString); else qDebug() << QString("Simplified actions for altering table '%1':\n").arg(window->partItem()->name()) + actionsDebugString + "\n"; return true; } bool AlterTableTester::checkActions(KexiWindow* window) { QString actionsDebugString; if (!getActionsDump(window, actionsDebugString)) return false; bool result = checkInternal(window, actionsDebugString, "endActions", true /*skipColonsAndStripWhiteSpace*/); qDebug() << QString("Actions check for table '%1': %2").arg(window->partItem()->name()) .arg(result ? "OK" : "Failed"); return result; } bool AlterTableTester::saveTableDesign(KexiWindow* window) { KexiTableDesignerInterface* designerIface = dynamic_cast(window->selectedView()); if (!designerIface) return false; tristate result = designerIface->executeRealAlterTable(); if (true != result) { showError(QString("Saving design of table '%1' failed.").arg(window->partItem()->name())); return false; } return true; } bool AlterTableTester::getTableDataDump(KexiWindow* window, QString& dataString) { KexiTableDesignerInterface* designerIface = dynamic_cast(window->selectedView()); if (!designerIface) return false; QMap args; QTextStream ts(&dataString, IO_WriteOnly); args["textStream"] = KexiUtils::ptrToString(&ts); args["destinationType"] = "file"; args["delimiter"] = "\t"; args["textQuote"] = "\""; args["itemId"] = QString::number( prj->dbConnection()->tableSchema(window->partItem()->name())->id()); if (!KexiInternalPart::executeCommand("org.kexi-project.importexport.csv", win, "KexiCSVExport", &args)) { showError("Error exporting table contents."); return false; } return true; } bool AlterTableTester::showTableData(KexiWindow* window, bool copyToClipboard) { QString dataString; if (!getTableDataDump(window, dataString)) return false; if (copyToClipboard) QApplication::clipboard()->setText(dataString); else qDebug() << QString("Contents of table '%1':\n").arg(window->partItem()->name()) + dataString + "\n"; return true; } bool AlterTableTester::checkTableData(KexiWindow* window) { QString dataString; if (!getTableDataDump(window, dataString)) return false; bool result = checkInternal(window, dataString, "endTableData", false /*!skipColonsAndStripWhiteSpace*/); qDebug() << QString("Table '%1' contents: %2").arg(window->partItem()->name()) .arg(result ? "OK" : "Failed"); return result; } bool AlterTableTester::closeWindow(KexiWindow* window) { if (!window) return true; QString name = window->partItem()->name(); tristate result = true == win->closeDialog(window, true/*layoutTaskBar*/, true/*doNotSaveChanges*/); qDebug() << QString("Closing window for table '%1': %2").arg(name) .arg(result == true ? "OK" : (result == false ? "Failed" : "Cancelled")); return result == true; } //! Processes test file tristate AlterTableTester::run(bool *closeAppRequested) { Q_ASSERT(closeAppRequested); *closeAppRequested = false; while (!m_finishedCopying) qApp->processEvents(300); qDebug() << "Database copied to temporary: " << dbFilename; if (!checkItemsNumber(2)) return false; tristate res = win->openProject(dbFilename, 0); if (true != res) return res; prj = win->project(); //open table in design mode res = readLineFromTestFile("designTable"); if (true != res) return ~res; QString tableName(testFileLine[1]); KexiPart::Item *item = prj->itemForMimeType("kexi/table", tableName); if (!item) { showError(QString("No such table '%1'").arg(tableName)); return false; } bool openingCancelled; KexiWindow* window = win->openObject(item, Kexi::DesignViewMode, &openingCancelled); if (!window) { showError(QString("Could not open table '%1'").arg(item->name())); return false; } KexiTableDesignerInterface* designerIface = dynamic_cast(window->selectedView()); if (!designerIface) return false; //dramatic speedup: temporary hide the window and propeditor QWidget * propeditor = KexiUtils::findFirstChild(qApp->mainWidget(), "KexiPropertyEditorView"); if (propeditor) propeditor->hide(); window->hide(); bool designTable = true; while (!testFileStream.atEnd()) { res = readLineFromTestFile(); if (true != res) return ~res; QString command(testFileLine[0]); if (designTable) { //subcommands available within "designTable" commands if (command == "endDesign") { if (!checkItemsNumber(1)) return false; //end of the design session: unhide the window and propeditor window->show(); if (propeditor) propeditor->show(); designTable = false; continue; } else if (command == "removeField") { if (!checkItemsNumber(2)) return false; bool ok; int row = getNumber(testFileLine[1], &ok) - 1; if (!ok) return false; designerIface->deleteRow(row, true); continue; } else if (command == "insertField") { if (!checkItemsNumber(3)) return false; bool ok; int row = getNumber(testFileLine[1], &ok) - 1; if (!ok) return false; designerIface->insertField(row, testFileLine[2], true); continue; } else if (command == "insertEmptyRecord") { if (!checkItemsNumber(2)) return false; bool ok; int row = getNumber(testFileLine[1], &ok) - 1; if (!ok) return false; designerIface->insertEmptyRecord(row, true); continue; } else if (command == "changeFieldProperty") { if (!checkItemsNumber(5) || !changeFieldProperty(designerIface)) return false; continue; } else if (command.startsWith("i=")) { bool ok; variableI = command.mid(2).toInt(&ok); if (!ok) { showError(QString("Invalid variable initialization '%1'").arg(command)); return false; } continue; } else if (command.startsWith("i++")) { variableI++; continue; } } else { //top-level commands available outside of "designTable" if (command == "showSchema") { if (!checkItemsNumber(1, 2) || !showSchema(window, testFileLine[1] == "clipboard")) return false; continue; } else if (command == "checkSchema") { if (!checkItemsNumber(1) || !checkSchema(window)) return false; continue; } else if (command == "showActions") { if (!checkItemsNumber(1, 2) || !showActions(window, testFileLine[1] == "clipboard")) return false; continue; } else if (command == "checkActions") { if (!checkItemsNumber(1) || !checkActions(window)) return false; continue; } else if (command == "saveTableDesign") { if (!checkItemsNumber(1) || !saveTableDesign(window)) return false; continue; } else if (command == "showTableData") { if (!checkItemsNumber(1, 2) || !showTableData(window, testFileLine[1] == "clipboard")) return false; continue; } else if (command == "checkTableData") { if (!checkItemsNumber(1) || !checkTableData(window)) return false; continue; } } //common commands if (command == "stop") { if (!checkItemsNumber(1)) return false; qDebug() << QString("Test STOPPED at line %1.").arg(testLineNumber); break; } else if (command == "closeWindow") { if (!checkItemsNumber(1) || !closeWindow(window)) return false; else window = 0; continue; } else if (command == "quit") { if (!checkItemsNumber(1) || !closeWindow(window)) return false; *closeAppRequested = true; qDebug() << QString("Quitting the application..."); break; } else { showError(QString("No such command '%1'").arg(command)); return false; } } return true; } //--------------------------------------- int quit(int result) { testFile.close(); if (newArgv) { delete [] newArgv[0]; delete [] newArgv[1]; delete [] newArgv; } return result; } int main(int argc, char *argv[]) { // args: <.altertable test filename> if (argc < 2) { qWarning() << "Please specify test filename.\nOptions: \n" "\t-close - closes the main window when test finishes"; return quit(1); } // options: const bool closeOnFinish = argc > 2 && 0 == qstrcmp(argv[1], "-close"); // open test file testFilename = argv[argc-1]; testFile.setName(testFilename); if (!testFile.open(IO_ReadOnly)) { qWarning() << QString("Opening test file %1 failed.").arg(testFilename); return quit(1); } //load db name testFileStream.setDevice(&testFile); tristate res = readLineFromTestFile("openDatabase"); if (true != res) return quit(~res ? 0 : 1); origDbFilename = testFileLine[1]; dbFilename = origDbFilename + ".tmp"; newArgc = 2; newArgv = new char*[newArgc]; newArgv[0] = qstrdup(argv[0]); newArgv[1] = 0; KAboutData aboutdata; aboutdata.setProgramName("Kexi Alter Table Test"); int result = KexiMainWindow::create(newArgc, newArgv, aboutdata); if (!qApp) return quit(result); win = KexiMainWindow::self(); AlterTableTester tester; //QObject::connect(win, SIGNAL(projectOpened()), &tester, SLOT(run())); bool closeAppRequested; res = tester.run(&closeAppRequested); if (true != res) { if (false == res) qWarning() << QString("Running test for file '%1' failed.").arg(testFilename); return quit(res == false ? 1 : 0); } qDebug() << QString("Tests from file '%1': OK").arg(testFilename); result = (closeOnFinish || closeAppRequested) ? 0 : qApp->exec(); quit(result); return result; } diff --git a/src/widget/utils/kexidatetimeformatter.cpp b/src/widget/utils/kexidatetimeformatter.cpp index 7a71a779c..9900ecfaa 100644 --- a/src/widget/utils/kexidatetimeformatter.cpp +++ b/src/widget/utils/kexidatetimeformatter.cpp @@ -1,467 +1,470 @@ /* This file is part of the KDE project Copyright (C) 2006-2016 Jarosław Staniek This program is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "kexidatetimeformatter.h" #include #include #include +#include class KexiDateFormatter::Private { public: Private() {} //! Input mask generated using the formatter settings. Can be used in QLineEdit::setInputMask(). QString inputMask; //! Order of date sections Order order; //! 4 or 2 digits bool longYear; bool monthWithLeadingZero, dayWithLeadingZero; //! Date format used in toString() QString qtFormat; //! Used in fromString(const QString&) to convert string back to QDate int yearpos, monthpos, daypos; QString separator; }; class KexiTimeFormatter::Private { public: Private() - : hmsRegExp(new QRegExp( - QLatin1String("(\\d*):(\\d*):(\\d*).*( am| pm){,1}"), Qt::CaseInsensitive)) - , hmRegExp(new QRegExp( - QLatin1String("(\\d*):(\\d*).*( am| pm){,1}"), Qt::CaseInsensitive)) + : hmsRegExp(new QRegularExpression( + QLatin1String("(\\d*):(\\d*):(\\d*).*( am| pm){,1}"), QRegularExpression::CaseInsensitiveOption)) + , hmRegExp(new QRegularExpression( + QLatin1String("(\\d*):(\\d*).*( am| pm){,1}"), QRegularExpression::CaseInsensitiveOption)) { } ~Private() { delete hmsRegExp; delete hmRegExp; } //! Input mask generated using the formatter settings. Can be used in QLineEdit::setInputMask(). QString inputMask; //! 12 or 12h bool is24h; bool hoursWithLeadingZero; //! Time format used in toString(). //! @todo KEXI3 port this to QLocale: Notation from KLocale::setTimeFormat() is used. //! @todo KEXI3 Qt5's QTime/QDate::fromString() differs from KLocale::setTimeFormat() QString outputFormat; //! Used in fromString(const QString&) to convert string back to QTime int hourpos, minpos, secpos, ampmpos; - QRegExp *hmsRegExp, *hmRegExp; + QRegularExpression *hmsRegExp, *hmRegExp; }; KexiDateFormatter::KexiDateFormatter() : d(new Private) { // use "short date" format system settings //! @todo allow to override the format using column property and/or global app settings QLocale locale; QString df(locale.dateFormat(QLocale::ShortFormat)); if (df.length() > 2) d->separator = df.mid(2, 1); else d->separator = "-"; const int separatorLen = d->separator.length(); QString yearMask("9999"); QString yearDateFormat("yyyy"); QString monthDateFormat("MM"); QString dayDateFormat("dd"); //for setting up d->dateFormat bool ok = df.length() >= 8; int yearpos, monthpos, daypos; //result of df.find() if (ok) {//look at % variables //! @todo more variables are possible here, see QDate::toString() docs yearpos = df.indexOf("%y", 0, Qt::CaseInsensitive); //&y or %y d->longYear = !(yearpos >= 0 && df.mid(yearpos + 1, 1) == "y"); if (!d->longYear) { yearMask = "99"; yearDateFormat = "yy"; } monthpos = df.indexOf("%m", 0, Qt::CaseSensitive); //%m or %n d->monthWithLeadingZero = true; if (monthpos < 0) { monthpos = df.indexOf("%n", 0, Qt::CaseInsensitive); d->monthWithLeadingZero = false; monthDateFormat = "M"; } daypos = df.indexOf("%d", 0, Qt::CaseSensitive);//%d or %e d->dayWithLeadingZero = true; if (daypos < 0) { daypos = df.indexOf("%e", 0, Qt::CaseInsensitive); d->dayWithLeadingZero = false; dayDateFormat = "d"; } ok = (yearpos >= 0 && monthpos >= 0 && daypos >= 0); } d->order = YMD; //default if (ok) { if (yearpos < monthpos && monthpos < daypos) { //will be set in "default: YMD" } else if (yearpos < daypos && daypos < monthpos) { d->order = YDM; //! @todo use QRegExp (to replace %Y by %1, etc.) instead of hardcoded "%1%299%399" //! because df may contain also other characters d->inputMask = yearMask + d->separator + QLatin1String("99") + d->separator + QLatin1String("99"); d->qtFormat = yearDateFormat + d->separator + dayDateFormat + d->separator + monthDateFormat; d->yearpos = 0; d->daypos = yearMask.length() + separatorLen; d->monthpos = d->daypos + 2 + separatorLen; } else if (daypos < monthpos && monthpos < yearpos) { d->order = DMY; d->inputMask = QLatin1String("99") + d->separator + QLatin1String("99") + d->separator + yearMask; d->qtFormat = dayDateFormat + d->separator + monthDateFormat + d->separator + yearDateFormat; d->daypos = 0; d->monthpos = 2 + separatorLen; d->yearpos = d->monthpos + 2 + separatorLen; } else if (monthpos < daypos && daypos < yearpos) { d->order = MDY; d->inputMask = QLatin1String("99") + d->separator + QLatin1String("99") + d->separator + yearMask; d->qtFormat = monthDateFormat + d->separator + dayDateFormat + d->separator + yearDateFormat; d->monthpos = 0; d->daypos = 2 + separatorLen; d->yearpos = d->daypos + 2 + separatorLen; } else ok = false; } if (!ok || d->order == YMD) {//default: YMD d->inputMask = yearMask + d->separator + QLatin1String("99") + d->separator + QLatin1String("99"); d->qtFormat = yearDateFormat + d->separator + monthDateFormat + d->separator + dayDateFormat; d->yearpos = 0; d->monthpos = yearMask.length() + separatorLen; d->daypos = d->monthpos + 2 + separatorLen; } d->inputMask += ";_"; } KexiDateFormatter::~KexiDateFormatter() { delete d; } QDate KexiDateFormatter::fromString(const QString& str, bool *ok) const { bool thisOk = true; if (!ok) { ok = &thisOk; } int year = str.mid(d->yearpos, d->longYear ? 4 : 2).toInt(ok); if (!*ok) { return QDate(); } if (year < 30) {//2000..2029 year = 2000 + year; } else if (year < 100) {//1930..1999 year = 1900 + year; } const int month = str.mid(d->monthpos, 2).toInt(ok); if (!*ok) { return QDate(); } const int day = str.mid(d->daypos, 2).toInt(ok); if (!*ok) { return QDate(); } const QDate date(year, month, day); if (!date.isValid()) { *ok = false; return QDate(); } *ok = true; return date; } QVariant KexiDateFormatter::stringToVariant(const QString& str, bool *ok) const { if (isEmpty(str)) { if (ok) { *ok = true; } return QVariant(); } const QDate date(fromString(str, ok)); if (date.isValid()) { return date; } return QVariant(); } bool KexiDateFormatter::isEmpty(const QString& str) const { if (str.isEmpty()) { return true; } QString s(str); return s.remove(d->separator).trimmed().isEmpty(); } QString KexiDateFormatter::inputMask() const { return d->inputMask; } QString KexiDateFormatter::separator() const { return d->separator; } QString KexiDateFormatter::toString(const QDate& date) const { return date.toString(d->qtFormat); } //------------------------------------------------ KexiTimeFormatter::KexiTimeFormatter() : d(new Private) { QLocale locale; QString tf(locale.timeFormat(QLocale::ShortFormat)); //d->hourpos, d->minpos, d->secpos; are result of tf.indexOf() QString hourVariable, minVariable, secVariable; //detect position of HOUR section: find %H or %k or %I or %l d->is24h = true; d->hoursWithLeadingZero = true; d->hourpos = tf.indexOf("%H", 0, Qt::CaseSensitive); if (d->hourpos >= 0) { d->is24h = true; d->hoursWithLeadingZero = true; } else { d->hourpos = tf.indexOf("%k", 0, Qt::CaseSensitive); if (d->hourpos >= 0) { d->is24h = true; d->hoursWithLeadingZero = false; } else { d->hourpos = tf.indexOf("%I", 0, Qt::CaseSensitive); if (d->hourpos >= 0) { d->is24h = false; d->hoursWithLeadingZero = true; } else { d->hourpos = tf.indexOf("%l", 0, Qt::CaseSensitive); if (d->hourpos >= 0) { d->is24h = false; d->hoursWithLeadingZero = false; } } } } d->minpos = tf.indexOf("%M", 0, Qt::CaseSensitive); d->secpos = tf.indexOf("%S", 0, Qt::CaseSensitive); //can be -1 d->ampmpos = tf.indexOf("%p", 0, Qt::CaseSensitive); //can be -1 if (d->hourpos < 0 || d->minpos < 0) { //set default: hr and min are needed, sec are optional tf = "%H:%M:%S"; d->is24h = true; d->hoursWithLeadingZero = false; d->hourpos = 0; d->minpos = 3; d->secpos = d->minpos + 3; d->ampmpos = -1; } hourVariable = tf.mid(d->hourpos, 2); d->inputMask = tf; d->inputMask.replace(hourVariable, "99"); d->inputMask.replace("%M", "99"); d->inputMask.replace("%S", "00"); //optional d->inputMask.replace("%p", "AA"); //am or pm d->inputMask += ";_"; d->outputFormat = tf; } KexiTimeFormatter::~KexiTimeFormatter() { delete d; } QTime KexiTimeFormatter::fromString(const QString& str) const { QTime time; int hour, min, sec; bool pm = false; - + QRegularExpressionMatch matchHms = d->hmsRegExp->match(str); + QRegularExpressionMatch matchHm = d->hmRegExp->match(str); bool tryWithoutSeconds = true; + if (d->secpos >= 0) { - if (-1 != d->hmsRegExp->indexIn(str)) { - hour = d->hmsRegExp->cap(1).toInt(); - min = d->hmsRegExp->cap(2).toInt(); - sec = d->hmsRegExp->cap(3).toInt(); + if (-1 != matchHms.capturedStart()) { + hour = matchHms.captured(1).toInt(); + min = matchHms.captured(2).toInt(); + sec = matchHms.captured(3).toInt(); if (d->ampmpos >= 0 && d->hmsRegExp->captureCount() > 3) - pm = d->hmsRegExp->cap(4).trimmed().toLower() == "pm"; + pm = matchHms.captured(4).trimmed().toLower() == "pm"; tryWithoutSeconds = false; } } if (tryWithoutSeconds) { - if (-1 == d->hmRegExp->indexIn(str)) + if (-1 == matchHm.capturedStart()) return QTime(99, 0, 0); - hour = d->hmRegExp->cap(1).toInt(); - min = d->hmRegExp->cap(2).toInt(); + hour = matchHm.captured(1).toInt(); + min = matchHm.captured(2).toInt(); sec = 0; if (d->ampmpos >= 0 && d->hmRegExp->captureCount() > 2) - pm = d->hmsRegExp->cap(4).toLower() == "pm"; + pm = matchHm.captured(4).toLower() == "pm"; } if (pm && hour < 12) hour += 12; //PM time = QTime(hour, min, sec); return time; } QVariant KexiTimeFormatter::stringToVariant(const QString& str, bool *ok) { if (isEmpty(str)) return QVariant(); const QTime time(fromString(str)); if (time.isValid()) { if (ok) { *ok = true; } return time; } if (ok) { *ok = false; } return QVariant(); } bool KexiTimeFormatter::isEmpty(const QString& str) const { QString s(str); return s.remove(':').trimmed().isEmpty(); } QString KexiTimeFormatter::toString(const QTime& time) const { if (!time.isValid()) return QString(); QString s(d->outputFormat); if (d->is24h) { if (d->hoursWithLeadingZero) s.replace("%H", QString::fromLatin1(time.hour() < 10 ? "0" : "") + QString::number(time.hour())); else s.replace("%k", QString::number(time.hour())); } else { int time12 = (time.hour() > 12) ? (time.hour() - 12) : time.hour(); if (d->hoursWithLeadingZero) s.replace("%I", QString::fromLatin1(time12 < 10 ? "0" : "") + QString::number(time12)); else s.replace("%l", QString::number(time12)); } s.replace("%M", QString::fromLatin1(time.minute() < 10 ? "0" : "") + QString::number(time.minute())); if (d->secpos >= 0) s.replace("%S", QString::fromLatin1(time.second() < 10 ? "0" : "") + QString::number(time.second())); if (d->ampmpos >= 0) s.replace("%p", time.hour() >= 12 ? xi18nc("afternoon", "pm") : xi18nc("before noon", "am")); return s; } QString KexiTimeFormatter::inputMask() const { return d->inputMask; } //------------------------------------------------ QString KexiDateTimeFormatter::inputMask(const KexiDateFormatter& dateFormatter, const KexiTimeFormatter& timeFormatter) { QString mask(dateFormatter.inputMask()); mask.truncate(dateFormatter.inputMask().length() - 2); return mask + " " + timeFormatter.inputMask(); } QDateTime KexiDateTimeFormatter::fromString( const KexiDateFormatter& dateFormatter, const KexiTimeFormatter& timeFormatter, const QString& str) { QString s(str.trimmed()); const int timepos = s.indexOf(' '); const bool emptyTime = timepos >= 0 && timeFormatter.isEmpty(s.mid(timepos + 1)); if (emptyTime) s = s.left(timepos); if (timepos > 0 && !emptyTime) { return QDateTime( dateFormatter.fromString(s.left(timepos)), timeFormatter.fromString(s.mid(timepos + 1)) ); } else { return QDateTime( dateFormatter.fromString(s), QTime(0, 0, 0) ); } } QString KexiDateTimeFormatter::toString(const KexiDateFormatter &dateFormatter, const KexiTimeFormatter &timeFormatter, const QDateTime &value) { if (value.isValid()) return dateFormatter.toString(value.date()) + ' ' + timeFormatter.toString(value.time()); return QString(); } bool KexiDateTimeFormatter::isEmpty(const KexiDateFormatter& dateFormatter, const KexiTimeFormatter& timeFormatter, const QString& str) { int timepos = str.indexOf(' '); const bool emptyTime = timepos >= 0 && timeFormatter.isEmpty(str.mid(timepos + 1)); return timepos >= 0 && dateFormatter.isEmpty(str.left(timepos)) && emptyTime; } bool KexiDateTimeFormatter::isValid(const KexiDateFormatter& dateFormatter, const KexiTimeFormatter& timeFormatter, const QString& str) { int timepos = str.indexOf(' '); const bool emptyTime = timepos >= 0 && timeFormatter.isEmpty(str.mid(timepos + 1)); if (timepos >= 0 && dateFormatter.isEmpty(str.left(timepos)) && emptyTime) { //empty date/time is valid return true; } return timepos >= 0 && dateFormatter.fromString(str.left(timepos)).isValid() && (emptyTime /*date without time is also valid*/ || timeFormatter.fromString(str.mid(timepos + 1)).isValid()); }