diff --git a/src/plugins/forms/kexiformscrollview.cpp b/src/plugins/forms/kexiformscrollview.cpp index e88c8c7b6..4bd59cd9b 100644 --- a/src/plugins/forms/kexiformscrollview.cpp +++ b/src/plugins/forms/kexiformscrollview.cpp @@ -1,770 +1,770 @@ /* This file is part of the KDE project Copyright (C) 2004 Cedric Pasteur 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 "kexiformscrollview.h" #include "KexiFormScrollAreaWidget.h" #include "widgets/kexidbform.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include class Q_DECL_HIDDEN KexiFormScrollView::Private { public: Private(KexiFormScrollView * view, bool preview_) : q(view) , resizingEnabled(true) , preview(preview_) , scrollBarPolicySet(false) , scrollViewNavPanel(0) , scrollViewNavPanelVisible(false) , mainAreaWidget(0) , currentLocalSortColumn(-1) /* no column */ - , localSortOrder(Qt::AscendingOrder) + , localSortOrder(KDbOrderByColumn::SortOrder::Ascending) , previousRecord(0) { } void setHorizontalScrollBarPolicyDependingOnNavPanel() { q->setHorizontalScrollBarPolicy( (scrollViewNavPanel && scrollViewNavPanelVisible) ? Qt::ScrollBarAlwaysOn : Qt::ScrollBarAsNeeded); } KexiFormScrollView * const q; bool resizingEnabled; QFont helpFont; QColor helpColor; QTimer delayedResize; //! for refreshContentsSizeLater() Qt::ScrollBarPolicy verticalScrollBarPolicy; Qt::ScrollBarPolicy horizontalScrollBarPolicy; bool preview; bool scrollBarPolicySet; bool outerAreaVisible; KexiRecordNavigator* scrollViewNavPanel; bool scrollViewNavPanelVisible; //!< Needed because visibility depends on form's visibility but we want to know earlier QMargins viewportMargins; QWidget *mainAreaWidget; KFormDesigner::Form *form; int currentLocalSortColumn; - Qt::SortOrder localSortOrder; + KDbOrderByColumn::SortOrder localSortOrder; //! Used in selectCellInternal() to avoid fetching the same record twice KDbRecordData *previousRecord; }; KexiFormScrollView::KexiFormScrollView(QWidget *parent, bool preview) : QScrollArea(parent) , KexiRecordNavigatorHandler() , KexiSharedActionClient() , KexiDataAwareObjectInterface() , KexiFormDataProvider() , KexiFormEventHandler() , d(new Private(this, preview)) { setObjectName("KexiFormScrollView"); setAttribute(Qt::WA_StaticContents, true); setFrameStyle(QFrame::StyledPanel|QFrame::Sunken); if (!d->preview) { QPalette pal(viewport()->palette()); pal.setBrush(viewport()->backgroundRole(), pal.brush(QPalette::Mid)); viewport()->setPalette(pal); } const QColor fc = palette().color(QPalette::WindowText); const QColor bc = viewport()->palette().color(QPalette::Window); d->helpColor = KexiUtils::blendedColors(fc, bc, 1, 2); d->helpFont = font(); d->helpFont.setPointSize(d->helpFont.pointSize() * 3); setFocusPolicy(Qt::WheelFocus); d->outerAreaVisible = true; d->delayedResize.setSingleShot(true); connect(&(d->delayedResize), SIGNAL(timeout()), this, SLOT(refreshContentsSize())); if (d->preview) { //! @todo allow to hide navigator d->scrollViewNavPanel = new KexiRecordNavigator(*this, this); } else { KexiFormScrollAreaWidget *scrollAreaWidget = new KexiFormScrollAreaWidget(this); setWidget(scrollAreaWidget); connect(scrollAreaWidget, SIGNAL(resized()), this, SIGNAL(resized())); } m_navPanel = recordNavigator(); //copy this pointer from KexiFormScrollView if (d->preview) { setRecordNavigatorVisible(true); refreshContentsSizeLater(); } m_contextMenu = new QMenu(this); m_contextMenu->setObjectName("m_contextMenu"); } KexiFormScrollView::~KexiFormScrollView() { if (m_owner) delete m_data; m_data = 0; delete d; } int KexiFormScrollView::recordsPerPage() const { //! @todo return 10; } void KexiFormScrollView::selectCellInternal(int previousRecord, int previousColumn) { Q_UNUSED(previousRecord); Q_UNUSED(previousColumn); //m_currentRecord is already set by KexiDataAwareObjectInterface::setCursorPosition() if (m_currentRecord) { if (m_currentRecord != d->previousRecord) { fillDataItems(m_currentRecord, cursorAtNewRecord()); d->previousRecord = m_currentRecord; QWidget *w = 0; if (m_curColumn >= 0 && m_curColumn < dbFormWidget()->orderedDataAwareWidgets()->count()) { w = dbFormWidget()->orderedDataAwareWidgets()->at(m_curColumn); } if (w) { w->setFocus(); // re-focus, as we could have lost focus, e.g. when navigator button was clicked // select all KexiFormDataItemInterface *iface = dynamic_cast(w); //! @todo add option for not selecting the field if (iface) { iface->selectAllOnFocusIfNeeded(); } } } } else { d->previousRecord = 0; } } void KexiFormScrollView::ensureCellVisible(int record, int column) { Q_UNUSED(record); Q_UNUSED(column); //! @todo } void KexiFormScrollView::ensureColumnVisible(int col) { Q_UNUSED(col); //! @todo } void KexiFormScrollView::moveToRecordRequested(int r) { //! @todo selectRecord(r); } void KexiFormScrollView::moveToLastRecordRequested() { //! @todo selectLastRecord(); } void KexiFormScrollView::moveToPreviousRecordRequested() { //! @todo selectPreviousRecord(); } void KexiFormScrollView::moveToNextRecordRequested() { //! @todo selectNextRecord(); } void KexiFormScrollView::moveToFirstRecordRequested() { //! @todo selectFirstRecord(); } void KexiFormScrollView::clearColumnsInternal(bool repaint) { Q_UNUSED(repaint); //! @todo } -Qt::SortOrder KexiFormScrollView::currentLocalSortOrder() const +KDbOrderByColumn::SortOrder KexiFormScrollView::currentLocalSortOrder() const { return d->localSortOrder; } int KexiFormScrollView::currentLocalSortColumn() const { return d->currentLocalSortColumn; } -void KexiFormScrollView::setLocalSortOrder(int column, Qt::SortOrder order) +void KexiFormScrollView::setLocalSortOrder(int column, KDbOrderByColumn::SortOrder order) { d->currentLocalSortColumn = column; d->localSortOrder = order; } void KexiFormScrollView::sortColumnInternal(int col, int order) { KexiDataAwareObjectInterface::sortColumnInternal(col, order); } void KexiFormScrollView::updateGUIAfterSorting(int previousRecord) { Q_UNUSED(previousRecord); //! @todo } void KexiFormScrollView::createEditor(int record, int column, const QString& addText, CreateEditorFlags flags) { Q_UNUSED(addText); Q_UNUSED(flags); if (record < 0) { qWarning() << "RECORD NOT SPECIFIED!" << record; return; } if (isReadOnly()) { qWarning() << "DATA IS READ ONLY!"; return; } if (this->column(column)->isReadOnly()) { qWarning() << "COL IS READ ONLY!"; return; } if (recordEditing() >= 0 && record != recordEditing()) { if (!acceptRecordEditing()) { return; } } //! @todo const bool startRecordEditing = recordEditing() == -1; //remember if we're starting record edit if (startRecordEditing) { //we're starting record editing session m_data->clearRecordEditBuffer(); setRecordEditing(record); //indicate on the vheader that we are editing: if (verticalHeader()) { updateVerticalHeaderSection(currentRecord()); } if (isInsertingEnabled() && record == recordCount()) { //we should know that we are in state "new record editing" m_newRecordEditing = true; //'insert' record editing: show another record after that: m_data->append(m_insertRecord); //new empty insert item m_insertRecord = m_data->createItem(); updateWidgetContentsSize(); } } m_editor = editor(column); //m_dataItems.at(col); if (!m_editor) return; if (startRecordEditing) { recordNavigator()->showEditingIndicator(true); //emit recordEditingStarted(record); } } KexiDataItemInterface *KexiFormScrollView::editor(int col, bool ignoreMissingEditor) { Q_UNUSED(ignoreMissingEditor); if (!m_data || col < 0 || col >= columnCount()) return 0; return dynamic_cast(dbFormWidget()->orderedDataAwareWidgets()->at(col)); } void KexiFormScrollView::editorShowFocus(int record, int column) { Q_UNUSED(record); Q_UNUSED(column); //! @todo } void KexiFormScrollView::updateCell(int record, int column) { Q_UNUSED(record); Q_UNUSED(column); //! @todo } void KexiFormScrollView::updateCurrentCell() { } void KexiFormScrollView::updateRecord(int record) { Q_UNUSED(record) //! @todo } void KexiFormScrollView::updateWidgetContents() { //! @todo } void KexiFormScrollView::updateWidgetContentsSize() { //! @todo } void KexiFormScrollView::slotRecordRepaintRequested(KDbRecordData* data) { Q_UNUSED(data); //! @todo } void KexiFormScrollView::slotRecordInserted(KDbRecordData* data, bool repaint) { Q_UNUSED(data); Q_UNUSED(repaint); //! @todo } void KexiFormScrollView::slotRecordInserted(KDbRecordData* data, int record, bool repaint) { Q_UNUSED(data); Q_UNUSED(record); Q_UNUSED(repaint); //! @todo } void KexiFormScrollView::slotRecordsDeleted(const QList &) { //! @todo } KexiDBForm* KexiFormScrollView::dbFormWidget() const { return qobject_cast(d->preview ? widget() : mainAreaWidget()); } int KexiFormScrollView::columnCount() const { return dbFormWidget()->orderedDataAwareWidgets()->count(); //m_dataItems.count(); } int KexiFormScrollView::recordCount() const { return KexiDataAwareObjectInterface::recordCount(); } int KexiFormScrollView::currentRecord() const { return KexiDataAwareObjectInterface::currentRecord(); } void KexiFormScrollView::setForm(KFormDesigner::Form *form) { d->form = form; } KFormDesigner::Form* KexiFormScrollView::form() const { return d->form; } bool KexiFormScrollView::columnEditable(int col) { #if 0 //qDebug() << "col=" << col; foreach(KexiFormDataItemInterface *dataItemIface, m_dataItems) { qDebug() << (dynamic_cast(dataItemIface) ? dynamic_cast(dataItemIface)->objectName() : "") << " " << dataItemIface->dataSource(); } //qDebug() << "-- focus widgets --"; foreach(QWidget* widget, *dbFormWidget()->orderedFocusWidgets()) { qDebug() << widget->objectName(); } //qDebug() << "-- data-aware widgets --"; foreach(QWidget *widget, *dbFormWidget()->orderedDataAwareWidgets()) { qDebug() << widget->objectName(); } #endif KexiFormDataItemInterface *item = dynamic_cast( dbFormWidget()->orderedDataAwareWidgets()->at(col)); if (!item || item->isReadOnly()) return false; return KexiDataAwareObjectInterface::columnEditable(col); } void KexiFormScrollView::valueChanged(KexiDataItemInterface* item) { if (!item) return; //only signal start editing when no record editing was started already /*qDebug() << "** editedItem=" << (dbFormWidget()->editedItem ? dbFormWidget()->editedItem->value().toString() : QString()) << ", " << (item ? item->value().toString() : QString());*/ if (dbFormWidget()->editedItem != item) { //qDebug() << "**>>> dbFormWidget()->editedItem = dynamic_cast(item)"; dbFormWidget()->editedItem = dynamic_cast(item); startEditCurrentCell(); } KexiFormDataItemInterface *formItem = dynamic_cast(item); if (formItem) { fillDuplicatedDataItems(formItem, item->value()); QWidget *widget = dynamic_cast(item); if (widget) { //value changed: clear 'default value' mode (e.g. a blue italic text) formItem->setDisplayDefaultValue(widget, false); } } } bool KexiFormScrollView::cursorAtNewRecord() const { return isInsertingEnabled() && (m_currentRecord == m_insertRecord || m_newRecordEditing); } void KexiFormScrollView::lengthExceeded(KexiDataItemInterface *item, bool lengthExceeded) { showLengthExceededMessage(item, lengthExceeded); } void KexiFormScrollView::updateLengthExceededMessage(KexiDataItemInterface *item) { showUpdateForLengthExceededMessage(item); } void KexiFormScrollView::initDataContents() { KexiDataAwareObjectInterface::initDataContents(); if (isPreviewing()) { //! @todo here we can react if user wanted to show the navigator setRecordNavigatorVisible(m_data); recordNavigator()->setEnabled(m_data); if (m_data) { recordNavigator()->setEditingIndicatorEnabled(!isReadOnly()); recordNavigator()->showEditingIndicator(false); } dbFormWidget()->updateReadOnlyFlags(); } } KDbTableViewColumn* KexiFormScrollView::column(int col) { const int id = fieldNumberForColumn(col); return (id >= 0) ? m_data->column(id) : 0; } bool KexiFormScrollView::shouldDisplayDefaultValueForItem(KexiFormDataItemInterface* itemIface) const { return cursorAtNewRecord() && !itemIface->columnInfo()->field()->defaultValue().isNull() && !itemIface->columnInfo()->field()->isAutoIncrement(); // default value defined } bool KexiFormScrollView::cancelEditor() { KexiFormDataItemInterface *itemIface = dynamic_cast(m_editor); if (!itemIface) { return false; } if (m_errorMessagePopup) m_errorMessagePopup->close(); itemIface->undoChanges(); const bool displayDefaultValue = shouldDisplayDefaultValueForItem(itemIface); // now disable/enable "display default value" if needed (do it after setValue(), before setValue() turns it off) if (itemIface->hasDisplayedDefaultValue() != displayDefaultValue) itemIface->setDisplayDefaultValue(dynamic_cast(itemIface), displayDefaultValue); fillDuplicatedDataItems(itemIface, m_editor->value()); // this will clear editor pointer and close message popup (if present) return KexiDataAwareObjectInterface::cancelEditor(); } void KexiFormScrollView::updateAfterCancelRecordEditing() { foreach(KexiFormDataItemInterface *dataItemIface, m_dataItems) { QWidget *w = dynamic_cast(dataItemIface); if (w) { //qDebug() << w->metaObject()->className() << w->objectName(); const bool displayDefaultValue = shouldDisplayDefaultValueForItem(dataItemIface); dataItemIface->undoChanges(); if (dataItemIface->hasDisplayedDefaultValue() != displayDefaultValue) dataItemIface->setDisplayDefaultValue(w, displayDefaultValue); } } recordNavigator()->showEditingIndicator(false); dbFormWidget()->editedItem = 0; KexiFormDataItemInterface *item = dynamic_cast(focusWidget()); if (item) { item->selectAllOnFocusIfNeeded(); } } void KexiFormScrollView::updateAfterAcceptRecordEditing() { if (!m_currentRecord) return; recordNavigator()->showEditingIndicator(false); dbFormWidget()->editedItem = 0; //update visible data because there could be auto-filled (eg. autonumber) fields fillDataItems(m_currentRecord, cursorAtNewRecord()); d->previousRecord = m_currentRecord; KexiFormDataItemInterface *item = dynamic_cast(focusWidget()); if (item) { item->selectAllOnFocusIfNeeded(); } } int KexiFormScrollView::fieldNumberForColumn(int col) { KexiFormDataItemInterface *item = dynamic_cast( dbFormWidget()->orderedDataAwareWidgets()->at(col)); if (!item) return -1; KexiFormDataItemInterfaceToIntMap::ConstIterator it(m_fieldNumbersForDataItems.find(item)); return it != m_fieldNumbersForDataItems.constEnd() ? (int)it.value() : -1; } void KexiFormScrollView::beforeSwitchView() { m_editor = 0; } void KexiFormScrollView::refreshContentsSize() { if (!widget()) return; if (d->preview) { setVerticalScrollBarPolicy(d->verticalScrollBarPolicy); //setHorizontalScrollBarPolicy(d->horizontalScrollBarPolicy); d->scrollBarPolicySet = false; updateScrollBars(); } else { // Ensure there is always space to resize Form int w = viewport()->width(); int h = viewport()->height(); bool change = false; const int delta_x = 300; const int delta_y = 300; if ((widget()->width() + delta_x * 2 / 3) > w) { w = widget()->width() + delta_x; change = true; } else if ((w - widget()->width()) > delta_x) { w = widget()->width() + delta_x; change = true; } if ((widget()->height() + delta_y * 2 / 3) > h) { h = widget()->height() + delta_y; change = true; } else if ((h - widget()->height()) > delta_y) { h = widget()->height() + delta_y; change = true; } if (change) { widget()->resize(w, h); } updateScrollBars(); setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); d->setHorizontalScrollBarPolicyDependingOnNavPanel(); } updateScrollBars(); //qDebug() << widget()->size() << d->form->widget()->size() << dbFormWidget()->size(); if (!d->preview) { widget()->resize(dbFormWidget()->size() + QSize(300, 300)); } else { widget()->resize(viewport()->size()); } //only clear cmd history when KexiScrollView::refreshContentsSizeLater() has been called if (!d->preview && sender() == delayedResizeTimer()) { if (d->form) d->form->clearUndoStack(); } } void KexiFormScrollView::handleDataWidgetAction(const QString& actionName) { QWidget *w = focusWidget(); KexiFormDataItemInterface *item = 0; while (w) { item = dynamic_cast(w); if (item) break; w = w->parentWidget(); } if (item) item->handleAction(actionName); } void KexiFormScrollView::copySelection() { handleDataWidgetAction("edit_copy"); } void KexiFormScrollView::cutSelection() { handleDataWidgetAction("edit_cut"); } void KexiFormScrollView::paste() { handleDataWidgetAction("edit_paste"); } int KexiFormScrollView::lastVisibleRecord() const { //! @todo unimplemented for now, this will be used for continuous forms return -1; } QScrollBar* KexiFormScrollView::verticalScrollBar() const { return QScrollArea::verticalScrollBar(); } void KexiFormScrollView::setRecordNavigatorVisible(bool visible) { if (d->scrollViewNavPanel) { d->scrollViewNavPanel->setVisible(visible); d->scrollViewNavPanelVisible = visible; } } bool KexiFormScrollView::isOuterAreaVisible() const { return d->outerAreaVisible; } void KexiFormScrollView::setOuterAreaIndicatorVisible(bool visible) { d->outerAreaVisible = visible; } bool KexiFormScrollView::isResizingEnabled() const { return d->resizingEnabled; } void KexiFormScrollView::setResizingEnabled(bool enabled) { d->resizingEnabled = enabled; } void KexiFormScrollView::refreshContentsSizeLater() { if (!d->scrollBarPolicySet) { d->scrollBarPolicySet = true; d->verticalScrollBarPolicy = verticalScrollBarPolicy(); d->horizontalScrollBarPolicy = horizontalScrollBarPolicy(); } setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); d->setHorizontalScrollBarPolicyDependingOnNavPanel(); updateScrollBars(); d->delayedResize.start(100); } void KexiFormScrollView::setHBarGeometry(QScrollBar & hbar, int x, int y, int w, int h) { if (d->scrollViewNavPanel && d->scrollViewNavPanel->isVisible()) { d->scrollViewNavPanel->setHBarGeometry(hbar, x, y, w, h); } else { hbar.setGeometry(x, y, w, h); } } KexiRecordNavigator* KexiFormScrollView::recordNavigator() const { return d->scrollViewNavPanel; } bool KexiFormScrollView::isPreviewing() const { return d->preview; } const QTimer *KexiFormScrollView::delayedResizeTimer() const { return &(d->delayedResize); } void KexiFormScrollView::setViewportMargins(const QMargins &margins) { QScrollArea::setViewportMargins(margins); d->viewportMargins = margins; } QMargins KexiFormScrollView::viewportMargins() const { return d->viewportMargins; } void KexiFormScrollView::setMainAreaWidget(QWidget* widget) { d->mainAreaWidget = widget; } QWidget* KexiFormScrollView::mainAreaWidget() const { return d->mainAreaWidget; } QRect KexiFormScrollView::viewportGeometry() const { return viewport()->geometry(); } void KexiFormScrollView::updateVerticalHeaderSection(int section) { Q_UNUSED(section); //! @todo implement when header is used } diff --git a/src/plugins/forms/kexiformscrollview.h b/src/plugins/forms/kexiformscrollview.h index 449b9785c..208e4e08d 100644 --- a/src/plugins/forms/kexiformscrollview.h +++ b/src/plugins/forms/kexiformscrollview.h @@ -1,357 +1,357 @@ /* This file is part of the KDE project Copyright (C) 2004 Cedric Pasteur 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. */ #ifndef KEXIFORMSCROLLVIEW_H #define KEXIFORMSCROLLVIEW_H #include "kexiformutils_export.h" #include #include #include #include #include #include #include #include class KexiRecordNavigator; class KexiDBForm; namespace KFormDesigner { class Form; } //! @short A widget for displaying a form view in a scrolled area. /** Users can resize the form's main widget, according to grid settings. * The content is resized so the widget can be further resized. * This class also implements: * - record navigation handling (KexiRecordNavigatorHandler) * - shared actions handling (KexiSharedActionClient) * - data-aware behaviour (KexiDataAwareObjectInterface) * - data provider bound to data-aware widgets (KexiFormDataProvider) * * @see KexiTableView */ class KEXIFORMUTILS_EXPORT KexiFormScrollView : public QScrollArea, public KexiRecordNavigatorHandler, public KexiSharedActionClient, public KexiDataAwareObjectInterface, public KexiFormDataProvider, public KexiFormEventHandler { Q_OBJECT KEXI_DATAAWAREOBJECTINTERFACE public: KexiFormScrollView(QWidget *parent, bool preview); virtual ~KexiFormScrollView(); void setForm(KFormDesigner::Form *form); KFormDesigner::Form* form() const; //! Needed to avoid conflict with QWidget::data(). inline KDbTableViewData* data() const { return KexiDataAwareObjectInterface::data(); } /*! Reimplemented from KexiDataAwareObjectInterface for checking 'readOnly' flag from a widget ('readOnly' flag from data member is still checked though). */ virtual bool columnEditable(int col); /*! \return number of visible columns in this view. There can be a number of duplicated columns defined, so columnCount() can return greater or smaller number than dataColumns(). */ virtual int columnCount() const; //! @return the number of records in the data set (if data set is present). virtual int recordCount() const; //! \return number of the currently selected record number or -1. virtual int currentRecord() const; /*! \return column information for column number \a col. Reimplemented for KexiDataAwareObjectInterface: column data corresponding to widget number is used here (see fieldNumberForColumn()). */ virtual KDbTableViewColumn* column(int col); /*! \return field number within data model connected to a data-aware widget at column \a col. */ virtual int fieldNumberForColumn(int col); /*! @internal Used by KexiFormView in view switching. */ void beforeSwitchView(); /*! \return last record visible on the screen (counting from 0). The returned value is guaranteed to be smaller or equal to currentRecord() or -1 if there are no records. Implemented for KexiDataAwareObjectInterface. */ //! @todo unimplemented for now, this will be used for continuous forms virtual int lastVisibleRecord() const; /*! \return vertical scrollbar. Implemented for KexiDataAwareObjectInterface. */ virtual QScrollBar* verticalScrollBar() const; KexiDBForm* dbFormWidget() const; //! @return true if snapping to grid is enabled. The defalt value is false. bool isSnapToGridEnabled() const; bool isResizingEnabled() const; void setResizingEnabled(bool enabled); void setRecordNavigatorVisible(bool visible); bool isOuterAreaVisible() const; void setOuterAreaIndicatorVisible(bool visible); void refreshContentsSizeLater(); KexiRecordNavigator* recordNavigator() const; bool isPreviewing() const; QMargins viewportMargins() const; void setViewportMargins(const QMargins &margins); //! @return widget displaying contents of the main area. QWidget* mainAreaWidget() const; //! Sets widget for displaying contents of the main area. void setMainAreaWidget(QWidget* widget); //! temporary int leftMargin() const { return 0; } //! temporary int bottomMargin() const { return 0; } //! temporary void updateScrollBars() {} /*! @return geometry of the viewport, i.e. the scrollable area, minus any scrollbars, etc. Implementation for KexiDataAwareObjectInterface. */ virtual QRect viewportGeometry() const; public Q_SLOTS: //! Implementation for KexiDataAwareObjectInterface //! \return arbitraty value of 10. virtual int recordsPerPage() const; //! Implementation for KexiDataAwareObjectInterface virtual void ensureCellVisible(int record, int col); //! Implementation for KexiDataAwareObjectInterface virtual void ensureColumnVisible(int col); virtual void moveToRecordRequested(int r); virtual void moveToLastRecordRequested(); virtual void moveToPreviousRecordRequested(); virtual void moveToNextRecordRequested(); virtual void moveToFirstRecordRequested(); virtual void addNewRecordRequested() { KexiDataAwareObjectInterface::addNewRecordRequested(); } /*! Cancels changes made to the currently active editor. Reverts the editor's value to old one. \return true on success or false on failure (e.g. when editor does not exist) */ virtual bool cancelEditor(); public Q_SLOTS: /*! Clear command history right after final resize. */ void refreshContentsSize(); /*! Handles verticalScrollBar()'s valueChanged(int) signal. Called when vscrollbar's value has been changed. */ //! @todo unused for now, will be used for continuous forms virtual void verticalScrollBarValueChanged(int v) { KexiDataAwareObjectInterface::verticalScrollBarValueChanged(v); } Q_SIGNALS: void itemChanged(KDbRecordData* data, int record, int column); void itemChanged(KDbRecordData* data, int record, int column, const QVariant &oldValue); void itemDeleteRequest(KDbRecordData* data, int record, int column); void currentItemDeleteRequest(); void newItemAppendedForAfterDeletingInSpreadSheetMode(); //!< does nothing void dataRefreshed(); void dataSet(KDbTableViewData *data); void itemSelected(KDbRecordData* data); void cellSelected(int record, int column); void sortedColumnChanged(int column); void recordEditingStarted(int record); void recordEditingTerminated(int record); void updateSaveCancelActions(); void reloadActions(); //! Emitted when the main widget area is being interactively resized. bool resized(); protected Q_SLOTS: //! Handles KDbTableViewData::recordRepaintRequested() signal virtual void slotRecordRepaintRequested(KDbRecordData* data); //! Handles KDbTableViewData::aboutToDeleteRecord() signal. Prepares info for slotRecordDeleted(). virtual void slotAboutToDeleteRecord(KDbRecordData* data, KDbResultInfo* result, bool repaint) { KexiDataAwareObjectInterface::slotAboutToDeleteRecord(data, result, repaint); } //! Handles KDbTableViewData::recordDeleted() signal to repaint when needed. virtual void slotRecordDeleted() { KexiDataAwareObjectInterface::slotRecordDeleted(); } //! Handles KDbTableViewData::recordInserted() signal to repaint when needed. virtual void slotRecordInserted(KDbRecordData* data, bool repaint); //! Like above, not db-aware version virtual void slotRecordInserted(KDbRecordData* data, int record, bool repaint); virtual void slotRecordsDeleted(const QList&); virtual void slotDataDestroying() { KexiDataAwareObjectInterface::slotDataDestroying(); } /*! Reloads data for this widget. Handles KDbTableViewData::reloadRequested() signal. */ virtual void reloadData() { KexiDataAwareObjectInterface::reloadData(); } //! Copy current selection to a clipboard (e.g. cell) virtual void copySelection(); //! Cut current selection to a clipboard (e.g. cell) virtual void cutSelection(); //! Paste current clipboard contents (e.g. to a cell) virtual void paste(); protected: //! Implementation for KexiDataAwareObjectInterface virtual void clearColumnsInternal(bool repaint); //! Implementation for KexiDataAwareObjectInterface - virtual Qt::SortOrder currentLocalSortOrder() const; + virtual KDbOrderByColumn::SortOrder currentLocalSortOrder() const; //! Implementation for KexiDataAwareObjectInterface virtual int currentLocalSortColumn() const; //! Implementation for KexiDataAwareObjectInterface. Visually does nothing //! but remembers index of the currently sorted column and order. - virtual void setLocalSortOrder(int column, Qt::SortOrder order); + virtual void setLocalSortOrder(int column, KDbOrderByColumn::SortOrder order); //! Implementation for KexiDataAwareObjectInterface. //! Just calls KexiDataAwareObjectInterface's implementation. void sortColumnInternal(int col, int order = 0); //! Implementation for KexiDataAwareObjectInterface. //! Nothing to do here. Record navigator is already updated. virtual void updateGUIAfterSorting(int previousRecord); //! Implementation for KexiDataAwareObjectInterface virtual void createEditor(int record, int column, const QString& addText = QString(), CreateEditorFlags flags = DefaultCreateEditorFlags); //! Implementation for KexiDataAwareObjectInterface virtual KexiDataItemInterface *editor(int col, bool ignoreMissingEditor = false); //! Implementation for KexiDataAwareObjectInterface virtual void editorShowFocus(int record, int column); /*! Implementation for KexiDataAwareObjectInterface Redraws specified cell. */ virtual void updateCell(int record, int column); /*! Redraws the current cell. Implemented after KexiDataAwareObjectInterface. */ virtual void updateCurrentCell(); /*! Implementation for KexiDataAwareObjectInterface Redraws all cells of specified record. */ virtual void updateRecord(int record); /*! Implementation for KexiDataAwareObjectInterface Updates contents of the widget. Just call update() here on your widget. */ virtual void updateWidgetContents(); /*! Implementation for KexiDataAwareObjectInterface Implementation for KexiDataAwareObjectInterface Updates widget's contents size e.g. using QScrollView::resizeContents(). */ virtual void updateWidgetContentsSize(); //! Reimplemented from KexiFormDataProvider. Reaction for change of \a item. virtual void valueChanged(KexiDataItemInterface* item); /*! Reimplemented from KexiFormDataProvider. \return information whether we're currently at new record or not. This can be used e.g. by data-aware widgets to determine if "(auto)" label should be displayed. */ virtual bool cursorAtNewRecord() const; /*! Implementation for KexiFormDataProvider. */ virtual void lengthExceeded(KexiDataItemInterface *item, bool lengthExceeded); /*! Implementation for KexiFormDataProvider. */ virtual void updateLengthExceededMessage(KexiDataItemInterface *item); //! Implementation for KexiDataAwareObjectInterface //! Called by KexiDataAwareObjectInterface::setCursorPosition() //! if cursor's position is really changed. virtual void selectCellInternal(int previousRecord, int previousColumn); /*! Reimplementation: used to refresh "editing indicator" visibility. */ virtual void initDataContents(); /*! @internal Updates record appearance after canceling record edit. Reimplemented from KexiDataAwareObjectInterface: just undoes changes for every data item. Used by cancelRecordEditing(). */ virtual void updateAfterCancelRecordEditing(); /*! @internal Updates record appearance after accepting record edit. Reimplemented from KexiDataAwareObjectInterface: just clears 'edit' indicator. Used by cancelRecordEditing(). */ virtual void updateAfterAcceptRecordEditing(); /*! @internal Used to invoke copy/paste/cut etc. actions at the focused widget's level. */ void handleDataWidgetAction(const QString& actionName); /*! @internal */ bool shouldDisplayDefaultValueForItem(KexiFormDataItemInterface* itemIface) const; virtual void setHBarGeometry(QScrollBar & hbar, int x, int y, int w, int h); const QTimer *delayedResizeTimer() const; //! Update section of vertical header virtual void updateVerticalHeaderSection(int section); private: class Private; Private * const d; }; #endif diff --git a/src/plugins/queries/kexiquerydesignerguieditor.cpp b/src/plugins/queries/kexiquerydesignerguieditor.cpp index 211e9c43d..a2e4e677b 100644 --- a/src/plugins/queries/kexiquerydesignerguieditor.cpp +++ b/src/plugins/queries/kexiquerydesignerguieditor.cpp @@ -1,1895 +1,1900 @@ /* 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 Q_DECL_HIDDEN 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? */ const KDbTableSchema *table = d->relations->tables()->value(tableName)->schema()->table(); KDbTableSchemaChangeListener::registerForChanges(d->conn, 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; } KexiQueryPartTempData * 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 KexiQueryPartTempData * 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") + const QString sortingString((*set)["sorting"].value().toString()); + if (sortingString != "ascending" && sortingString != "descending") { continue; + } + const KDbOrderByColumn::SortOrder sortOrder = sortingString == QLatin1String("ascending") + ? KDbOrderByColumn::SortOrder::Ascending : KDbOrderByColumn::SortOrder::Descending; 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"); + orderByColumns.appendField(currentField, sortOrder); 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->isVisible()) - orderByColumns.appendColumn(currentColumn, sortingString == "ascending"); + orderByColumns.appendColumn(currentColumn, sortOrder); else if (currentColumn->field()) - orderByColumns.appendField(currentColumn->field(), sortingString == "ascending"); + orderByColumns.appendField(currentColumn->field(), sortOrder); } } 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; KexiQueryPartTempData * 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) { + for (auto 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->isVisible()) { 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); + orderByColumn->sortOrder() == KDbOrderByColumn::SortOrder::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() { KexiQueryPartTempData * 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; 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 ((match = QRegularExpression("^(\\d{1,4})-(\\d{1,2})-(\\d{1,2})$").match(str)).hasMatch()) { valueExpr = KDbConstExpression(KDbToken::DATE_CONST, QDate::fromString( 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 ((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.leftRef(pos).toInt(&ok); if (!ok) return KDbExpression(); const int right = str.midRef(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; 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->message = xi18nc("@info", "Entered column alias %1 is not a valid identifier.", QString::fromLatin1(alias)); result->description = 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->message = 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->message = 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->message = xi18nc("@info", "Could not set criteria for %1", table == "*" ? table : field); else result->message = 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->message = 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/reports/kexidbreportdata.cpp b/src/plugins/reports/kexidbreportdata.cpp index 2287f3c5a..7b5ba9ebc 100644 --- a/src/plugins/reports/kexidbreportdata.cpp +++ b/src/plugins/reports/kexidbreportdata.cpp @@ -1,415 +1,416 @@ /* * Kexi Report Plugin * Copyright (C) 2007-2009 by Adam Pigg (adam@piggz.co.uk) * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see . */ #include "kexidbreportdata.h" #include #include +#include #include #include #include #include class Q_DECL_HIDDEN KexiDBReportData::Private { public: explicit Private(KDbConnection *pDb) : cursor(0), connection(pDb), originalSchema(0), copySchema(0) { } ~Private() { delete copySchema; delete originalSchema; } QString objectName; KDbCursor *cursor; KDbConnection *connection; KDbQuerySchema *originalSchema; KDbQuerySchema *copySchema; }; KexiDBReportData::KexiDBReportData (const QString &objectName, KDbConnection * pDb) : d(new Private(pDb)) { d->objectName = objectName; getSchema(); } KexiDBReportData::KexiDBReportData(const QString& objectName, const QString& pluginId, KDbConnection* pDb) : d(new Private(pDb)) { d->objectName = objectName; getSchema(pluginId); } void KexiDBReportData::setSorting(const QList& sorting) { if (d->copySchema) { if (sorting.isEmpty()) return; KDbOrderByColumnList order; for (int i = 0; i < sorting.count(); i++) { if (!order.appendField(d->copySchema, sorting[i].field(), - sorting[i].order() == Qt::AscendingOrder)) + KDbOrderByColumn::fromQt(sorting[i].order()))) { qWarning() << "Cannot set sort field" << i << sorting[i].field(); return; } } d->copySchema->setOrderByColumnList(order); } else { qWarning() << "Unable to sort null schema"; } } void KexiDBReportData::addExpression(const QString& field, const QVariant& value, char relation) { if (d->copySchema) { KDbField *fld = d->copySchema->findTableField(field); if (fld) { d->copySchema->addToWhereExpression(fld, value, KDbToken(relation)); } } else { qDebug() << "Unable to add expresstion to null schema"; } } KexiDBReportData::~KexiDBReportData() { close(); delete d; } bool KexiDBReportData::open() { if ( d->connection && d->cursor == 0 ) { if ( d->objectName.isEmpty() ) { return false; } else if ( d->copySchema) { qDebug() << "Opening cursor.." << *d->copySchema; d->cursor = d->connection->executeQuery ( d->copySchema, 1 ); } if ( d->cursor ) { qDebug() << "Moving to first record.."; return d->cursor->moveFirst(); } else return false; } return false; } bool KexiDBReportData::close() { if (d->cursor) { const bool ok = d->cursor->close(); d->connection->deleteCursor(d->cursor); d->cursor = nullptr; return ok; } return true; } bool KexiDBReportData::getSchema(const QString& pluginId) { if (d->connection) { delete d->originalSchema; d->originalSchema = 0; delete d->copySchema; d->copySchema = 0; if ((pluginId.isEmpty() || pluginId == "org.kexi-project.table") && d->connection->tableSchema(d->objectName)) { qDebug() << d->objectName << "is a table.."; d->originalSchema = new KDbQuerySchema(d->connection->tableSchema(d->objectName)); } else if ((pluginId.isEmpty() || pluginId == "org.kexi-project.query") && d->connection->querySchema(d->objectName)) { qDebug() << d->objectName << "is a query.."; qDebug() << *d->connection->querySchema(d->objectName); d->originalSchema = new KDbQuerySchema(*(d->connection->querySchema(d->objectName))); } if (d->originalSchema) { const KDbNativeStatementBuilder builder(d->connection); KDbEscapedString sql; if (builder.generateSelectStatement(&sql, d->originalSchema)) { qDebug() << "Original:" << sql; } else { qDebug() << "Original: ERROR"; } qDebug() << *d->originalSchema; d->copySchema = new KDbQuerySchema(*d->originalSchema); qDebug() << *d->copySchema; if (builder.generateSelectStatement(&sql, d->copySchema)) { qDebug() << "Copy:" << sql; } else { qDebug() << "Copy: ERROR"; } } return true; } return false; } QString KexiDBReportData::sourceName() const { return d->objectName; } int KexiDBReportData::fieldNumber ( const QString &fld ) const { if (!d->cursor || !d->cursor->query()) { return -1; } const KDbQueryColumnInfo::Vector fieldsExpanded( d->cursor->query()->fieldsExpanded(KDbQuerySchema::Unique)); for (int i = 0; i < fieldsExpanded.size() ; ++i) { if (0 == QString::compare(fld, fieldsExpanded[i]->aliasOrName(), Qt::CaseInsensitive)) { return i; } } return -1; } QStringList KexiDBReportData::fieldNames() const { if (!d->originalSchema) { return QStringList(); } QStringList names; const KDbQueryColumnInfo::Vector fieldsExpanded( d->originalSchema->fieldsExpanded(KDbQuerySchema::Unique)); for (int i = 0; i < fieldsExpanded.size(); i++) { //! @todo in some Kexi mode captionOrAliasOrName() would be used here (more user-friendly) names.append(fieldsExpanded[i]->aliasOrName()); } return names; } QVariant KexiDBReportData::value ( unsigned int i ) const { if ( d->cursor ) return d->cursor->value ( i ); return QVariant(); } QVariant KexiDBReportData::value ( const QString &fld ) const { int i = fieldNumber ( fld ); if (d->cursor && i >= 0) return d->cursor->value ( i ); return QVariant(); } bool KexiDBReportData::moveNext() { if ( d->cursor ) return d->cursor->moveNext(); return false; } bool KexiDBReportData::movePrevious() { if ( d->cursor ) return d->cursor->movePrev(); return false; } bool KexiDBReportData::moveFirst() { if ( d->cursor ) return d->cursor->moveFirst(); return false; } bool KexiDBReportData::moveLast() { if ( d->cursor ) return d->cursor->moveLast(); return false; } qint64 KexiDBReportData::at() const { if ( d->cursor ) return d->cursor->at(); return 0; } qint64 KexiDBReportData::recordCount() const { if ( d->copySchema ) { return KDb::recordCount ( d->copySchema ); } return 1; } static bool isInterpreterSupported(const QString &interpreterName) { return 0 == interpreterName.compare(QLatin1String("javascript"), Qt::CaseInsensitive) || 0 == interpreterName.compare(QLatin1String("qtscript"), Qt::CaseInsensitive); } QStringList KexiDBReportData::scriptList() const { QStringList scripts; if( d->connection) { QList scriptids = d->connection->objectIds(KexiPart::ScriptObjectType); QStringList scriptnames = d->connection->objectNames(KexiPart::ScriptObjectType); qDebug() << scriptids << scriptnames; //A blank entry scripts << ""; int i = 0; foreach(int id, scriptids) { qDebug() << "ID:" << id; tristate res; QString script; res = d->connection->loadDataBlock(id, &script, QString()); if (res == true) { QDomDocument domdoc; bool parsed = domdoc.setContent(script, false); QDomElement scriptelem = domdoc.namedItem("script").toElement(); if (parsed && !scriptelem.isNull()) { if (scriptelem.attribute("scripttype") == "object" && isInterpreterSupported(scriptelem.attribute("language"))) { scripts << scriptnames[i]; } } else { qDebug() << "Unable to parse script"; } } else { qDebug() << "Unable to loadDataBlock"; } ++i; } qDebug() << scripts; } return scripts; } QString KexiDBReportData::scriptCode(const QString& scriptname) const { QString scripts; if (d->connection) { QList scriptids = d->connection->objectIds(KexiPart::ScriptObjectType); QStringList scriptnames = d->connection->objectNames(KexiPart::ScriptObjectType); int i = 0; foreach(int id, scriptids) { qDebug() << "ID:" << id; tristate res; QString script; res = d->connection->loadDataBlock(id, &script, QString()); if (res == true) { QDomDocument domdoc; bool parsed = domdoc.setContent(script, false); if (! parsed) { qDebug() << "XML parsing error"; return QString(); } QDomElement scriptelem = domdoc.namedItem("script").toElement(); if (scriptelem.isNull()) { qDebug() << "script domelement is null"; return QString(); } QString interpretername = scriptelem.attribute("language"); qDebug() << scriptelem.attribute("scripttype"); qDebug() << scriptname << scriptnames[i]; if ((isInterpreterSupported(interpretername) && scriptelem.attribute("scripttype") == "module") || scriptname == scriptnames[i]) { scripts += '\n' + scriptelem.text().toUtf8(); } ++i; } else { qDebug() << "Unable to loadDataBlock"; } } } return scripts; } QStringList KexiDBReportData::dataSources() const { //Get the list of queries in the database QStringList qs; if (d->connection && d->connection->isConnected()) { QList tids = d->connection->tableIds(); qs << ""; for (int i = 0; i < tids.size(); ++i) { KDbTableSchema* tsc = d->connection->tableSchema(tids[i]); if (tsc) qs << tsc->name(); } QList qids = d->connection->queryIds(); qs << ""; for (int i = 0; i < qids.size(); ++i) { KDbQuerySchema* qsc = d->connection->querySchema(qids[i]); if (qsc) qs << qsc->name(); } } return qs; } KReportData* KexiDBReportData::create(const QString& source) const { return new KexiDBReportData(source, d->connection); } diff --git a/src/widget/dataviewcommon/kexidataawareobjectiface.cpp b/src/widget/dataviewcommon/kexidataawareobjectiface.cpp index 654b89c4c..58f33d414 100644 --- a/src/widget/dataviewcommon/kexidataawareobjectiface.cpp +++ b/src/widget/dataviewcommon/kexidataawareobjectiface.cpp @@ -1,1983 +1,1984 @@ /* This file is part of the KDE project Copyright (C) 2005-2015 Jarosław Staniek Based on KexiTableView code. Copyright (C) 2002 Till Busch Copyright (C) 2003 Lucijan Busch Copyright (C) 2003 Daniel Molkentin Copyright (C) 2003 Joseph Wenninger 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 "kexidataawareobjectiface.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace KexiUtils; KexiDataAwareObjectInterface::KexiDataAwareObjectInterface() { m_data = 0; m_owner = false; m_readOnly = -1; //don't know m_insertingEnabled = -1; //don't know m_isSortingEnabled = true; m_isFilteringEnabled = true; m_deletionPolicy = AskDelete; m_inside_acceptEditor = false; m_inside_acceptRecordEdit = false; m_acceptsRecordEditAfterCellAccepting = false; m_internal_acceptsRecordEditingAfterCellAccepting = false; m_contentsMousePressEvent_dblClick = false; m_navPanel = 0; m_initDataContentsOnShow = false; m_cursorPositionSetExplicityBeforeShow = false; m_insertRecord = 0; m_spreadSheetMode = false; m_navPanelEnabled = true; m_dropsAtRecordEnabled = false; m_updateEntireRecordWhenMovingToOtherRecord = false; m_dragIndicatorLine = -1; m_emptyRecordInsertingEnabled = false; m_contextMenu = 0; m_contextMenuEnabled = true; m_recordWillBeDeleted = -1; m_alsoUpdateNextRecord = false; m_verticalScrollBarValueChanged_enabled = true; m_scrollbarToolTipsEnabled = true; m_recentSearchDirection = KexiSearchAndReplaceViewInterface::Options::DefaultSearchDirection; m_lengthExceededMessageVisible = false; m_acceptRecordEditing_in_setCursorPosition_enabled = true; clearVariables(); } KexiDataAwareObjectInterface::~KexiDataAwareObjectInterface() { delete m_insertRecord; } void KexiDataAwareObjectInterface::clearVariables() { m_editor = 0; m_recordEditing = -1; m_newRecordEditing = false; m_curRecord = -1; m_curColumn = -1; m_currentRecord = 0; } void KexiDataAwareObjectInterface::setData(KDbTableViewData *data, bool owner) { const bool theSameData = m_data && m_data == data; if (m_owner && m_data && m_data != data/*don't destroy if it's the same*/) { qDebug() << "destroying old data (owned)"; delete m_data; //destroy old data m_data = 0; m_itemIterator = KDbTableViewDataIterator(); } m_owner = owner; m_data = data; if (m_data) m_itemIterator = m_data->begin(); //qDebug() << "using shared data"; //add columns clearColumnsInternal(false); // set column widths if (horizontalHeader()) { int i = 0; horizontalHeader()->setSectionResizeMode(QHeaderView::Interactive); // set before using resizeSection() foreach(KDbTableViewColumn *col, *m_data->columns()) { if (col->isVisible()) { int w = col->width(); if (w == 0) { w = KEXI_DEFAULT_DATA_COLUMN_WIDTH; //default col width in pixels } //! @todo add col width configuration and storage horizontalHeader()->resizeSection(i, w); i++; } } } if (verticalHeader()) { //TODO verticalHeader()->update(); verticalHeader()->setSectionResizeMode(QHeaderView::Fixed); verticalHeader()->headerDataChanged(Qt::Vertical, 0, data->count() - 1); } //!Change the following: if (m_data && m_data->count() == 0 && m_navPanel) m_navPanel->setCurrentRecordNumber(0 + 1); if (m_data && !theSameData) { //! @todo: store sorting settings? setSorting(-1); connectToReloadDataSlot(m_data, SIGNAL(reloadRequested())); QObject* thisObject = dynamic_cast(this); if (thisObject) { QObject::connect(m_data, SIGNAL(destroying()), thisObject, SLOT(slotDataDestroying())); QObject::connect(m_data, SIGNAL(recordsDeleted(QList)), thisObject, SLOT(slotRecordsDeleted(QList))); QObject::connect(m_data, SIGNAL(aboutToDeleteRecord(KDbRecordData*,KDbResultInfo*,bool)), thisObject, SLOT(slotAboutToDeleteRecord(KDbRecordData*,KDbResultInfo*,bool))); QObject::connect(m_data, SIGNAL(recordDeleted()), thisObject, SLOT(slotRecordDeleted())); QObject::connect(m_data, SIGNAL(recordInserted(KDbRecordData*,bool)), thisObject, SLOT(slotRecordInserted(KDbRecordData*,bool))); QObject::connect(m_data, SIGNAL(recordInserted(KDbRecordData*,int,bool)), thisObject, SLOT(slotRecordInserted(KDbRecordData*,int,bool))); //not db-aware QObject::connect(m_data, SIGNAL(recordRepaintRequested(KDbRecordData*)), thisObject, SLOT(slotRecordRepaintRequested(KDbRecordData*))); QObject::connect(verticalScrollBar(), SIGNAL(valueChanged(int)), thisObject, SLOT(verticalScrollBarValueChanged(int))); } } if (!m_data) { cancelRecordEditing(); clearVariables(); } else { if (!m_insertRecord) {//first setData() call - add 'insert' item m_insertRecord = m_data->createItem(); } else {//just reinit m_insertRecord->resize(m_data->columnCount()); } } //update gui mode if (m_navPanel) { m_navPanel->setInsertingEnabled(m_data && isInsertingEnabled()); m_navPanel->setInsertingButtonVisible(m_data && isInsertingEnabled()); } initDataContents(); updateIndicesForVisibleValues(); if (m_data) /*emit*/ dataSet(m_data); } void KexiDataAwareObjectInterface::initDataContents() { m_editor = 0; if (m_navPanel) m_navPanel->setRecordCount(recordCount()); if (m_data && !m_cursorPositionSetExplicityBeforeShow) { //set current row: m_currentRecord = 0; int curRow = -1, curCol = -1; if (m_data->columnCount() > 0) { if (recordCount() > 0) { m_itemIterator = m_data->begin(); m_currentRecord = *m_itemIterator; curRow = 0; curCol = 0; } else {//no data if (isInsertingEnabled()) { m_currentRecord = m_insertRecord; curRow = 0; curCol = 0; } } } setCursorPosition(curRow, curCol, ForceSetCursorPosition); } ensureCellVisible(m_curRecord, m_curColumn); //OK? updateWidgetContents(); m_cursorPositionSetExplicityBeforeShow = false; /*emit*/ dataRefreshed(); } void KexiDataAwareObjectInterface::setSortingEnabled(bool set) { if (m_isSortingEnabled && !set) setSorting(-1); m_isSortingEnabled = set; /*emit*/ reloadActions(); } -void KexiDataAwareObjectInterface::setSorting(int column, Qt::SortOrder order) +void KexiDataAwareObjectInterface::setSorting(int column, KDbOrderByColumn::SortOrder order) { if (!m_data || !m_isSortingEnabled) return; setLocalSortOrder(column, order); m_data->setSorting(column, order); } int KexiDataAwareObjectInterface::dataSortColumn() const { if (m_data && m_isSortingEnabled) return m_data->sortColumn(); return -1; } -Qt::SortOrder KexiDataAwareObjectInterface::dataSortOrder() const +KDbOrderByColumn::SortOrder KexiDataAwareObjectInterface::dataSortOrder() const { - return m_data ? m_data->sortOrder() : Qt::AscendingOrder; + return m_data ? m_data->sortOrder() : KDbOrderByColumn::SortOrder::Ascending; } bool KexiDataAwareObjectInterface::sort() { if (!m_data || !m_isSortingEnabled) return false; if (recordCount() < 2) return true; if (!acceptRecordEditing()) return false; const int oldRow = m_curRecord; if (m_data->sortColumn() != -1) m_data->sort(); //locate current record if (!m_currentRecord) { m_itemIterator = m_data->begin(); m_currentRecord = *m_itemIterator; m_curRecord = 0; if (!m_currentRecord) return true; } if (m_currentRecord != m_insertRecord) { m_curRecord = m_data->indexOf(m_currentRecord); int jump = m_curRecord - oldRow; if (jump < 0) m_itemIterator -= -jump; else m_itemIterator += jump; } updateGUIAfterSorting(oldRow); editorShowFocus(m_curRecord, m_curColumn); if (m_navPanel) m_navPanel->setCurrentRecordNumber(m_curRecord + 1); return true; } void KexiDataAwareObjectInterface::sortAscending() { if (currentColumn() < 0) return; sortColumnInternal(currentColumn(), 1); } void KexiDataAwareObjectInterface::sortDescending() { if (currentColumn() < 0) return; sortColumnInternal(currentColumn(), -1); } void KexiDataAwareObjectInterface::sortColumnInternal(int col, int order) { //-select sorting bool asc; if (order == 0) {// invert - if (col == dataSortColumn() && dataSortOrder() == Qt::AscendingOrder) + if (col == dataSortColumn() && dataSortOrder() == KDbOrderByColumn::SortOrder::Ascending) { asc = false; // invert - else + } else { asc = true; + } } else { asc = (order == 1); } - const Qt::SortOrder prevSortOrder = currentLocalSortOrder(); + const KDbOrderByColumn::SortOrder prevSortOrder = currentLocalSortOrder(); const int prevSortColumn = currentLocalSortColumn(); - setSorting(col, asc ? Qt::AscendingOrder : Qt::DescendingOrder); + setSorting(col, asc ? KDbOrderByColumn::SortOrder::Ascending : KDbOrderByColumn::SortOrder::Descending); //-perform sorting if (!sort()) { setLocalSortOrder(prevSortColumn, prevSortOrder); //this will also remove indicator } if (col != prevSortColumn) { /*emit*/ sortedColumnChanged(col); } } bool KexiDataAwareObjectInterface::isInsertingEnabled() const { if (isReadOnly()) return false; if (m_insertingEnabled == 1 || m_insertingEnabled == 0) return (bool)m_insertingEnabled; if (!hasData()) return true; return m_data->isInsertingEnabled(); } void KexiDataAwareObjectInterface::setFilteringEnabled(bool set) { m_isFilteringEnabled = set; } bool KexiDataAwareObjectInterface::isDeleteEnabled() const { return (m_deletionPolicy != NoDelete) && !isReadOnly(); } void KexiDataAwareObjectInterface::setDeletionPolicy(DeletionPolicy policy) { m_deletionPolicy = policy; } void KexiDataAwareObjectInterface::setReadOnly(bool set) { if (isReadOnly() == set || (m_data && m_data->isReadOnly() && !set)) return; //not allowed! m_readOnly = (set ? 1 : 0); if (set) setInsertingEnabled(false); updateWidgetContents(); /*emit*/ reloadActions(); } bool KexiDataAwareObjectInterface::isReadOnly() const { if (!hasData()) return true; if (m_readOnly == 1 || m_readOnly == 0) return (bool)m_readOnly; if (!hasData()) return true; return m_data->isReadOnly(); } void KexiDataAwareObjectInterface::setInsertingEnabled(bool set) { if (isInsertingEnabled() == set || (m_data && !m_data->isInsertingEnabled() && set)) return; //not allowed! m_insertingEnabled = (set ? 1 : 0); if (m_navPanel) { m_navPanel->setInsertingEnabled(set); m_navPanel->setInsertingButtonVisible(set); } if (set) setReadOnly(false); updateWidgetContents(); /*emit*/ reloadActions(); } void KexiDataAwareObjectInterface::setSpreadSheetMode(bool set) { m_spreadSheetMode = set; setSortingEnabled(!set); setInsertingEnabled(!set); setAcceptsRecordEditAfterCellAccepting(set); setFilteringEnabled(!set); setEmptyRecordInsertingEnabled(set); m_navPanelEnabled = !set; } void KexiDataAwareObjectInterface::selectNextRecord() { selectRecord(qMin(recordCount() - 1 + (isInsertingEnabled() ? 1 : 0), m_curRecord + 1)); } void KexiDataAwareObjectInterface::selectPreviousPage() { selectRecord( qMax(0, m_curRecord - recordsPerPage()) ); } void KexiDataAwareObjectInterface::selectNextPage() { selectRecord( qMin( recordCount() - 1 + (isInsertingEnabled() ? 1 : 0), m_curRecord + recordsPerPage() ) ); } void KexiDataAwareObjectInterface::selectFirstRecord() { selectRecord(0); } void KexiDataAwareObjectInterface::selectLastRecord() { selectRecord(recordCount() > 0 ? (recordCount() - 1) : 0); } void KexiDataAwareObjectInterface::selectRecord(int record) { m_verticalScrollBarValueChanged_enabled = false; //disable tooltip setCursorPosition(record, -1); m_verticalScrollBarValueChanged_enabled = true; } void KexiDataAwareObjectInterface::selectPreviousRecord() { selectRecord(qMax(0, m_curRecord - 1)); } void KexiDataAwareObjectInterface::clearSelection() { int oldRow = m_curRecord; m_curRecord = -1; m_curColumn = -1; m_currentRecord = 0; updateRecord(oldRow); if (m_navPanel) m_navPanel->setCurrentRecordNumber(0); } // #define setCursorPosition_DEBUG void KexiDataAwareObjectInterface::setCursorPosition(int record, int col/*=-1*/, CursorPositionFlags flags) { int newRecord = record; int newCol = col; if (recordCount() <= 0) { if (isInsertingEnabled()) { m_currentRecord = m_insertRecord; newRecord = 0; if (col >= 0) newCol = col; else newCol = 0; } else { m_currentRecord = 0; m_curRecord = -1; m_curColumn = -1; return; } } if (col >= 0) { newCol = qMax(0, col); newCol = qMin(columnCount() - 1, newCol); } else { newCol = m_curColumn; //no changes newCol = qMax(0, newCol); //may not be < 0 ! } newRecord = qMax(0, record); newRecord = qMin(recordCount() - 1 + (isInsertingEnabled() ? 1 : 0), newRecord); // qDebug() << "setCursorPosition(): d->curRow=" << d->curRow << " oldRow=" << oldRow << " d->curCol=" << d->curCol << " oldCol=" << oldCol; const bool forceSet = flags & ForceSetCursorPosition; if (forceSet || m_curRecord != newRecord || m_curColumn != newCol) { #ifdef setCursorPosition_DEBUG qDebug() << QString("old:%1,%2 new:%3,%4").arg(m_curColumn) .arg(m_curRecord).arg(newcol).arg(newrow); #endif // cursor moved: get rid of editor if (m_editor) { if (!m_contentsMousePressEvent_dblClick && m_acceptRecordEditing_in_setCursorPosition_enabled) { if (!acceptEditor()) { return; } //update row num. again newRecord = qMin(recordCount() - 1 + (isInsertingEnabled() ? 1 : 0), newRecord); } } if (m_errorMessagePopup) { m_errorMessagePopup->animatedHide(); } if ((m_curRecord != newRecord || forceSet) && m_navPanel) {//update current row info m_navPanel->setCurrentRecordNumber(newRecord + 1); } // cursor moved to other row: end of row editing bool newRecordInserted = false; if (m_recordEditing >= 0 && m_curRecord != newRecord) { newRecordInserted = m_newRecordEditing; if (m_acceptRecordEditing_in_setCursorPosition_enabled && !acceptRecordEditing()) { //accepting failed: cancel setting the cursor return; } //update row number, because number of rows changed newRecord = qMin(recordCount() - 1 + (isInsertingEnabled() ? 1 : 0), newRecord); if (m_navPanel) m_navPanel->setCurrentRecordNumber(newRecord + 1); //refresh } //change position int oldRow = m_curRecord; int oldCol = m_curColumn; m_curRecord = newRecord; m_curColumn = newCol; //show editor-dependent focus, if we're changing the current column if (oldCol >= 0 && oldCol < columnCount() && m_curColumn != oldCol) { //find the editor for this column KexiDataItemInterface *edit = editor(oldCol); if (edit) { edit->hideFocus(); } } // position changed, so subsequent searching should be started from scratch // (e.g. from the current cell or the top-left cell) m_positionOfRecentlyFoundValue.exists = false; //show editor-dependent focus, if needed editorShowFocus(m_curRecord, m_curColumn); if (m_updateEntireRecordWhenMovingToOtherRecord) updateRecord(oldRow); else updateCell(oldRow, oldCol); if (m_updateEntireRecordWhenMovingToOtherRecord) updateRecord(m_curRecord); else updateCell(m_curRecord, m_curColumn); if (m_curColumn != oldCol || m_curRecord != oldRow || forceSet) {//ensure this is also refreshed if (!m_updateEntireRecordWhenMovingToOtherRecord) //only if entire row has not been updated updateCell(oldRow, m_curColumn); } //update row if (forceSet || m_curRecord != oldRow) { if (isInsertingEnabled() && m_curRecord == recordCount()) { #ifdef setCursorPosition_DEBUG qDebug() << "NOW insert item is current"; #endif m_currentRecord = m_insertRecord; m_itemIterator = KDbTableViewDataIterator(); } else { #ifdef setCursorPosition_DEBUG qDebug() << QString("NOW item at %1 (%2) is current") .arg(m_curRecord).arg((ulong)itemAt(m_curRecord)); int _i = 0; qDebug() << "m_curRecord:" << m_curRecord; for (KDbTableViewDataIterator ii = m_data->begin(); ii != m_data->end(); ++ii) { qDebug() << _i << (ulong)(*ii) << (ii == m_itemIterator ? "CURRENT" : "") << *(*ii); _i++; } qDebug() << "~" << m_curRecord << (ulong)(*m_itemIterator) << *(*m_itemIterator); #endif if ( !newRecordInserted && isInsertingEnabled() && m_currentRecord == m_insertRecord && m_curRecord == (recordCount() - 1)) { //moving from the 'insert item' to the last item m_itemIterator = m_data->begin(); m_itemIterator += (m_data->count() - 1); } else if (!newRecordInserted && !forceSet && m_currentRecord != m_insertRecord && 0 == m_curRecord) { m_itemIterator = m_data->begin(); } else if ( !newRecordInserted && !forceSet && m_currentRecord != m_insertRecord && oldRow >= 0 && (oldRow + 1) == m_curRecord) { ++m_itemIterator; // just move next } else if ( !newRecordInserted && !forceSet && m_currentRecord != m_insertRecord && oldRow >= 0 && (oldRow - 1) == m_curRecord) { --m_itemIterator; // just move back } else { //move at: m_itemIterator = m_data->begin(); m_itemIterator += m_curRecord; } if (!*m_itemIterator) { //sanity m_itemIterator = m_data->begin(); m_itemIterator += m_curRecord; } m_currentRecord = *m_itemIterator; #ifdef setCursorPosition_DEBUG qDebug() << "new~" << m_curRecord << (ulong)(*m_itemIterator) << (*m_itemIterator)->debugString(); #endif } } //quite clever: ensure the cell is visible: ensureCellVisible(m_curRecord, m_curColumn); /*emit*/ itemSelected(m_currentRecord); /*emit*/ cellSelected(m_curRecord, m_curColumn); selectCellInternal(oldRow, oldCol); } else { if (!(flags & DontEnsureCursorVisibleIfPositionUnchanged) && m_curRecord >= 0 && m_curRecord < recordCount() && m_curColumn >= 0 && m_curColumn < columnCount()) { // the same cell but may need a bit of scrolling to make it visible ensureCellVisible(m_curRecord, m_curColumn); } //qDebug() << "NO CHANGE"; } if (m_initDataContentsOnShow) { m_cursorPositionSetExplicityBeforeShow = true; } } void KexiDataAwareObjectInterface::selectCellInternal(int previousRecord, int previousColumn) { Q_UNUSED(previousRecord); Q_UNUSED(previousColumn); } bool KexiDataAwareObjectInterface::acceptRecordEditing() { if (m_recordEditing == -1 || /*sanity*/!m_data->recordEditBuffer() || m_inside_acceptRecordEdit) return true; if (m_inside_acceptEditor) { m_internal_acceptsRecordEditingAfterCellAccepting = true; return true; } QScopedValueRollback acceptRecordEditingSetter(m_inside_acceptRecordEdit, true); // avoid recursion m_internal_acceptsRecordEditingAfterCellAccepting = false; const int columnEditedBeforeAccepting = m_editor ? currentColumn() : -1; if (!acceptEditor()) { return false; } //qDebug() << "EDIT RECORD ACCEPTING..."; bool success = true; const bool inserting = m_newRecordEditing; if (m_data->recordEditBuffer()->isEmpty() && !m_newRecordEditing) { //qDebug() << "-- NOTHING TO ACCEPT!!!"; } else {//not empty edit buffer or new row to insert: if (m_newRecordEditing) { qDebug() << "-- INSERTING:" << *m_data->recordEditBuffer(); success = m_data->saveNewRecord(m_currentRecord); } else { if (success) { //accept changes for this row: qDebug() << "-- UPDATING:" << *m_data->recordEditBuffer(); qDebug() << "-- BEFORE:" << *m_currentRecord; success = m_data->saveRecordChanges(m_currentRecord); qDebug() << "-- AFTER:" << *m_currentRecord; } } } if (success) { //editing is finished: if (m_newRecordEditing) { //update current-item-iterator setCursorPosition(m_curRecord, -1, ForceSetCursorPosition); } m_recordEditing = -1; m_newRecordEditing = false; updateAfterAcceptRecordEditing(); qDebug() << "EDIT RECORD ACCEPTED:"; if (inserting) { //update navigator's data if (m_navPanel) m_navPanel->setRecordCount(recordCount()); } /*emit*/ recordEditingTerminated(m_curRecord); } else { int faultyColumn = -1; if (m_data->result().column >= 0 && m_data->result().column < columnCount()) faultyColumn = m_data->result().column; else if (columnEditedBeforeAccepting >= 0) faultyColumn = columnEditedBeforeAccepting; if (faultyColumn >= 0) { setCursorPosition(m_curRecord, faultyColumn); } const int button = showErrorMessageForResult(m_data->result()); if (KMessageBox::No == button) { //discard changes cancelRecordEditing(); } else { if (faultyColumn >= 0) { //edit this cell startEditCurrentCell(); } } } //indicate on the vheader that we are not editing if (verticalHeader()) { //qDebug() << currentRecord(); updateVerticalHeaderSection(currentRecord()); } return success; } bool KexiDataAwareObjectInterface::cancelRecordEditing() { if (!hasData()) return true; if (m_recordEditing == -1) return true; cancelEditor(); m_recordEditing = -1; m_alsoUpdateNextRecord = m_newRecordEditing; if (m_newRecordEditing) { m_newRecordEditing = false; beginRemoveItem(m_currentRecord, m_curRecord); //remove current edited row (it is @ the end of list) m_data->removeLast(); endRemoveItem(m_curRecord); //current item is now empty, last row m_currentRecord = m_insertRecord; //update visibility updateWidgetContents(); updateWidgetContentsSize(); //--no cancel action is needed for datasource, // because the row was not yet stored. } m_data->clearRecordEditBuffer(); updateAfterCancelRecordEditing(); //indicate on the vheader that we are not editing if (verticalHeader()) { //qDebug() << currentRecord(); updateVerticalHeaderSection(currentRecord()); } //! \todo (js): cancel changes for this row! qDebug() << "EDIT RECORD CANCELLED."; /*emit*/ recordEditingTerminated(m_curRecord); return true; } void KexiDataAwareObjectInterface::updateAfterCancelRecordEditing() { updateRecord(m_curRecord); if (m_alsoUpdateNextRecord) updateRecord(m_curRecord + 1); m_alsoUpdateNextRecord = false; } void KexiDataAwareObjectInterface::updateAfterAcceptRecordEditing() { updateRecord(m_curRecord); } void KexiDataAwareObjectInterface::removeEditor() { if (!m_editor) return; m_editor->hideWidget(); m_editor = 0; } bool KexiDataAwareObjectInterface::cancelEditor() { if (m_errorMessagePopup) { m_errorMessagePopup->animatedHide(); } if (!m_editor) return true; removeEditor(); return true; } bool KexiDataAwareObjectInterface::acceptEditor() { if (!hasData()) return true; if (!m_editor || m_inside_acceptEditor) return true; QScopedValueRollback acceptRecordEditingSetter(m_inside_acceptEditor, true); // avoid recursion QVariant newval; KDbValidator::Result res = KDbValidator::Ok; QString msg, desc; bool setNull = false; //autoincremented field can be omitted (left as null or empty) if we're inserting a new row const bool autoIncColumnCanBeOmitted = m_newRecordEditing && m_editor->field()->isAutoIncrement(); bool valueChanged = m_editor->valueChanged(); if (valueChanged) { if (!m_editor->valueIsValid()) { //used e.g. for date or time values - the value can be null but not necessary invalid res = KDbValidator::Error; //! @todo allow displaying user-defined warning showEditorContextMessage( m_editor, xi18nc("Question", "Error: %1?", m_editor->columnInfo()->field()->typeName()), KMessageWidget::Error, KMessageWidget::Up); } else if (m_editor->valueIsNull()) {//null value entered if (m_editor->field()->isNotNull() && !autoIncColumnCanBeOmitted) { qDebug() << "NULL NOT ALLOWED!"; res = KDbValidator::Error; msg = KDbValidator::messageColumnNotEmpty().arg(m_editor->field()->captionOrName()) + "\n\n" + KDbTableViewData::messageYouCanImproveData(); desc = xi18n("The column's constraint is declared as NOT NULL (required)."); } else { qDebug() << "NULL VALUE WILL BE SET"; //ok, just leave newval as NULL setNull = true; } } else if (m_editor->valueIsEmpty()) {//empty value entered if (m_editor->field()->hasEmptyProperty()) { if (m_editor->field()->isNotEmpty() && !autoIncColumnCanBeOmitted) { qDebug() << "EMPTY NOT ALLOWED!"; res = KDbValidator::Error; msg = KDbValidator::messageColumnNotEmpty().arg(m_editor->field()->captionOrName()) + "\n\n" + KDbTableViewData::messageYouCanImproveData(); desc = xi18n("The column's constraint is declared as NOT EMPTY (text should be filled)."); } else { qDebug() << "EMPTY VALUE WILL BE SET"; } } else { if (m_editor->field()->isNotNull() && !autoIncColumnCanBeOmitted) { qDebug() << "NEITHER NULL NOR EMPTY VALUE CAN BE SET!"; res = KDbValidator::Error; msg = KDbValidator::messageColumnNotEmpty().arg(m_editor->field()->captionOrName()) + "\n\n" + KDbTableViewData::messageYouCanImproveData(); desc = xi18n("The column's constraint is declared as NOT EMPTY and NOT NULL."); } else { qDebug() << "NULL VALUE WILL BE SET BECAUSE EMPTY IS NOT ALLOWED"; //ok, just leave newval as NULL setNull = true; } } } else { // try to fixup the value before accepting, e.g. trim the text if (!m_editor->fixup()) { res = KDbValidator::Error; } if (m_errorMessagePopup) { m_errorMessagePopup->animatedHide(); } if (res != KDbValidator::Ok) { //! @todo display message related to failed fixup if needed } //! @todo after fixup we may want to apply validation rules again } }//changed const int realFieldNumber = fieldNumberForColumn(m_curColumn); if (realFieldNumber < 0) { qWarning() << "fieldNumberForColumn(m_curColumn) < 0"; return false; } KDbTableViewColumn *currentTVColumn = column(m_curColumn); //try to get the value entered: if (res == KDbValidator::Ok) { if ( (!setNull && !valueChanged) || (m_editor->field()->type() != KDbField::Boolean && setNull && m_currentRecord->at(realFieldNumber).isNull())) { qDebug() << "VALUE NOT CHANGED."; removeEditor(); if (m_acceptsRecordEditAfterCellAccepting || m_internal_acceptsRecordEditingAfterCellAccepting) acceptRecordEditing(); return true; } if (!setNull) {//get the new value newval = m_editor->value(); //! @todo validation rules for this value? } //Check other validation rules: //1. check using validator KDbValidator *validator = currentTVColumn->validator(); if (validator) { res = validator->check(currentTVColumn->field()->captionOrName(), newval, &msg, &desc); } } //show the validation result if not OK: if (res == KDbValidator::Error) { if (!msg.isEmpty()) { if (desc.isEmpty()) KMessageBox::sorry(dynamic_cast(this), msg); else KMessageBox::detailedSorry(dynamic_cast(this), msg, desc); } } else if (res == KDbValidator::Warning) { //! @todo: message KMessageBox::messageBox(dynamic_cast(this), KMessageBox::Sorry, msg + "\n" + desc); } if (res == KDbValidator::Ok) { //2. check using signal //send changes to the backend QVariant visibleValue; if ( !newval.isNull()/* visible value should be null if value is null */ && currentTVColumn->visibleLookupColumnInfo()) { visibleValue = m_editor->visibleValue(); //visible value for lookup field } //should be also added to the buffer if (m_data->updateRecordEditBufferRef(m_currentRecord, m_curColumn, currentTVColumn, &newval, /*allowSignals*/true, currentTVColumn->visibleLookupColumnInfo() ? &visibleValue : 0)) { qDebug() << "------ EDIT BUFFER CHANGED TO:" << *m_data->recordEditBuffer(); } else { qDebug() << "------ CHANGE FAILED"; res = KDbValidator::Error; //now: there might be called cancelEditor() in updateRecordEditBuffer() handler, //if this is true, d->pEditor is NULL. if (m_editor && m_data->result().column >= 0 && m_data->result().column < columnCount()) { //move to faulty column (if m_editor is not cleared) setCursorPosition(m_curRecord, m_data->result().column); } if (!m_data->result().message.isEmpty()) { const int button = showErrorMessageForResult(m_data->result()); if (KMessageBox::No == button) { //discard changes cancelEditor(); if (m_acceptsRecordEditAfterCellAccepting) cancelRecordEditing(); return false; } } } } if (res == KDbValidator::Ok) { removeEditor(); /*emit*/ itemChanged(m_currentRecord, m_curRecord, m_curColumn, m_currentRecord->at(realFieldNumber)); /*emit*/ itemChanged(m_currentRecord, m_curRecord, m_curColumn); } if (res == KDbValidator::Ok) { if (m_acceptsRecordEditAfterCellAccepting || m_internal_acceptsRecordEditingAfterCellAccepting) { m_inside_acceptEditor = false; acceptRecordEditing(); m_inside_acceptEditor = true; } return true; } if (m_editor) { //allow to edit the cell again, (if m_pEditor is not cleared) if (m_editor->hasFocusableWidget()) { m_editor->showWidget(); m_editor->setFocus(); } } return false; } void KexiDataAwareObjectInterface::startEditCurrentCell(const QString &setText, CreateEditorFlags flags) { //qDebug() << "setText:" << setText; if (isReadOnly() || !columnEditable(m_curColumn)) return; if (m_editor) { if (m_editor->hasFocusableWidget()) { m_editor->showWidget(); m_editor->setFocus(); } } else { if (!setText.isEmpty()) { flags |= ReplaceOldValue; } createEditor(m_curRecord, m_curColumn, setText, flags); } } void KexiDataAwareObjectInterface::deleteAndStartEditCurrentCell() { if (isReadOnly() || !columnEditable(m_curColumn)) return; if (m_editor) {//if we've editor - just clear it m_editor->clear(); return; } if (m_curRecord < (recordCount() - 1) || !spreadSheetMode()) { ensureCellVisible(m_curRecord + 1, m_curColumn); } createEditor(m_curRecord, m_curColumn); if (!m_editor) return; m_editor->clear(); if (m_editor->acceptEditorAfterDeleteContents()) acceptEditor(); if (!m_editor || !m_editor->hasFocusableWidget()) updateCell(m_curRecord, m_curColumn); } void KexiDataAwareObjectInterface::deleteCurrentRecord() { if (m_newRecordEditing) {//we're editing fresh new row: just cancel this! cancelRecordEditing(); return; } if (!isDeleteEnabled() || !m_currentRecord || m_currentRecord == m_insertRecord) { return; } ensureCellVisible(m_curRecord, m_curColumn); if (!acceptRecordEditing()) return; switch (m_deletionPolicy) { case NoDelete: return; case ImmediateDelete: break; case AskDelete: if (KMessageBox::Yes != KMessageBox::questionYesNo( dynamic_cast(this), xi18n("Do you want to delete selected record?"), QString(), KGuiItem(xi18nc("@action:button", "&Delete Record"), KexiIconName("edit-table-delete-row")), KStandardGuiItem::cancel(), "AskBeforeDeleteRow"/*config entry*/, KMessageBox::Notify | KMessageBox::Dangerous)) { return; } break; case SignalDelete: /*emit*/ itemDeleteRequest(m_currentRecord, m_curRecord, m_curColumn); /*emit*/ currentItemDeleteRequest(); return; default: return; } if (!deleteItem(m_currentRecord)) {//nothing } } KDbRecordData* KexiDataAwareObjectInterface::insertEmptyRecord(int pos) { if (!acceptRecordEditing() || !isEmptyRecordInsertingEnabled() || (pos != -1 && pos >= (recordCount() + (isInsertingEnabled() ? 1 : 0)))) return 0; KDbRecordData *newRecord = m_data->createItem(); insertItem(newRecord, pos); return newRecord; } void KexiDataAwareObjectInterface::beginInsertItem(KDbRecordData *newRecord, int pos) { Q_UNUSED(newRecord); Q_UNUSED(pos); } void KexiDataAwareObjectInterface::endInsertItem(KDbRecordData *newRecord, int pos) { Q_UNUSED(newRecord); Q_UNUSED(pos); } void KexiDataAwareObjectInterface::insertItem(KDbRecordData *data, int pos) { const bool changeCurrentRecord = pos == -1 || pos == m_curRecord; if (changeCurrentRecord) { //change current record pos = (m_curRecord >= 0 ? m_curRecord : 0); m_currentRecord = data; m_curRecord = pos; } else if (m_curRecord >= pos) { m_curRecord++; } beginInsertItem(data, pos); m_data->insertRecord(data, pos, true /*repaint*/); // always update iterator since the list was modified... m_itemIterator = m_data->begin(); m_itemIterator += m_curRecord; endInsertItem(data, pos); } void KexiDataAwareObjectInterface::slotRecordInserted(KDbRecordData* data, bool repaint) { slotRecordInserted(data, m_data->indexOf(data), repaint); } void KexiDataAwareObjectInterface::slotRecordInserted(KDbRecordData * /*data*/, int pos, bool repaint) { if (repaint && (int)pos < recordCount()) { updateWidgetContentsSize(); updateAllVisibleRecordsBelow(pos); //update navigator's data if (m_navPanel) m_navPanel->setRecordCount(recordCount()); if (m_curRecord >= (int)pos) { //update editorShowFocus(m_curRecord, m_curColumn); } } } tristate KexiDataAwareObjectInterface::deleteAllRecords(bool ask, bool repaint) { if (!hasData()) return true; if (m_data->count() < 1) return true; if (ask) { QString tableName = m_data->dbTableName(); if (!tableName.isEmpty()) { tableName.prepend(" \""); tableName.append("\""); } if (KMessageBox::Cancel == KMessageBox::warningContinueCancel(dynamic_cast(this), xi18n("Do you want to clear the contents of table %1?", tableName), 0, KGuiItem(xi18nc("@action:button", "&Clear Contents"), koIcon("edit-table-clear")))) { return cancelled; } } cancelRecordEditing(); const bool repaintLater = repaint && m_spreadSheetMode; const int oldRows = recordCount(); bool res = m_data->deleteAllRecords(repaint && !repaintLater); if (res) { if (m_spreadSheetMode) { for (int i = 0; i < oldRows; i++) { m_data->append(m_data->createItem()); } } } if (repaintLater) m_data->reload(); return res; } void KexiDataAwareObjectInterface::clearColumns(bool repaint) { cancelRecordEditing(); m_data->clearInternal(); clearColumnsInternal(repaint); updateIndicesForVisibleValues(); if (repaint) updateWidgetContents(); } void KexiDataAwareObjectInterface::reloadData() { acceptRecordEditing(); if (m_curColumn >= 0 && m_curColumn < columnCount()) { //find the editor for this column KexiDataItemInterface *edit = editor(m_curColumn); if (edit) { edit->hideFocus(); } } clearVariables(); const QWidget* thisWidget = dynamic_cast(this); if (thisWidget && thisWidget->isVisible()) initDataContents(); else m_initDataContentsOnShow = true; } int KexiDataAwareObjectInterface::columnType(int col) { KDbTableViewColumn* c = m_data ? column(col) : 0; return c ? c->field()->type() : KDbField::InvalidType; } bool KexiDataAwareObjectInterface::columnEditable(int col) { KDbTableViewColumn* c = m_data ? column(col) : 0; return c ? (! c->isReadOnly()) : false; } QHeaderView* KexiDataAwareObjectInterface::horizontalHeader() const { return 0; } int KexiDataAwareObjectInterface::horizontalHeaderHeight() const { return 0; } QHeaderView* KexiDataAwareObjectInterface::verticalHeader() const { return 0; } int KexiDataAwareObjectInterface::recordCount() const { if (!hasData()) return 0; return m_data->count(); } int KexiDataAwareObjectInterface::columnCount() const { return dataColumns(); } int KexiDataAwareObjectInterface::dataColumns() const { if (!hasData()) return 0; return m_data->columnCount(); } QVariant KexiDataAwareObjectInterface::columnDefaultValue(int /*col*/) const { return QVariant(0); //! @todo return m_data->columns[col].defaultValue; } void KexiDataAwareObjectInterface::setAcceptsRecordEditAfterCellAccepting(bool set) { m_acceptsRecordEditAfterCellAccepting = set; } void KexiDataAwareObjectInterface::setDropsAtRecordEnabled(bool set) { if (!set) m_dragIndicatorLine = -1; if (m_dropsAtRecordEnabled && !set) { m_dropsAtRecordEnabled = false; updateWidgetContents(); } else { m_dropsAtRecordEnabled = set; } } void KexiDataAwareObjectInterface::setEmptyRecordInsertingEnabled(bool set) { m_emptyRecordInsertingEnabled = set; /*emit*/ reloadActions(); } void KexiDataAwareObjectInterface::slotAboutToDeleteRecord(KDbRecordData* data, KDbResultInfo* /*result*/, bool repaint) { if (repaint) { m_recordWillBeDeleted = m_data->indexOf(data); } } void KexiDataAwareObjectInterface::slotRecordDeleted() { if (m_recordWillBeDeleted >= 0) { if (m_recordWillBeDeleted > 0 && m_recordWillBeDeleted >= (recordCount() - 1) && !m_spreadSheetMode) m_recordWillBeDeleted = recordCount() - 1; //move up if it's the last row updateWidgetContentsSize(); if (!(m_spreadSheetMode && m_recordWillBeDeleted >= (recordCount() - 1))) setCursorPosition(m_recordWillBeDeleted, m_curColumn, ForceSetCursorPosition); updateAllVisibleRecordsBelow(m_curRecord); //needed for KexiTableView //update navigator's data if (m_navPanel) m_navPanel->setRecordCount(recordCount()); m_recordWillBeDeleted = -1; } } bool KexiDataAwareObjectInterface::beforeDeleteItem(KDbRecordData*) { //always return return true; } void KexiDataAwareObjectInterface::beginRemoveItem(KDbRecordData *data, int pos) { Q_UNUSED(data); Q_UNUSED(pos); } void KexiDataAwareObjectInterface::endRemoveItem(int pos) { Q_UNUSED(pos); } bool KexiDataAwareObjectInterface::deleteItem(KDbRecordData* data) { if (!data || !beforeDeleteItem(data)) return false; const int pos = m_data->indexOf(data); beginRemoveItem(data, pos); bool result = m_data->deleteRecord(data, true /*repaint*/); endRemoveItem(pos); if (!result) { showErrorMessageForResult(m_data->result()); return false; } if (m_spreadSheetMode) { //append empty row for spreadsheet mode insertItem(m_data->createItem(), m_data->count()); setCursorPosition(m_curRecord, m_curColumn, ForceSetCursorPosition); /*emit*/ newItemAppendedForAfterDeletingInSpreadSheetMode(); } return true; } KDbTableViewColumn* KexiDataAwareObjectInterface::column(int column) { return m_data->column(column); } bool KexiDataAwareObjectInterface::hasDefaultValueAt(const KDbTableViewColumn& tvcol) { if (m_recordEditing >= 0 && m_data->recordEditBuffer() && m_data->recordEditBuffer()->isDBAware()) { return m_data->recordEditBuffer()->hasDefaultValueAt(tvcol.columnInfo()); } return false; } const QVariant* KexiDataAwareObjectInterface::bufferedValueAt(int record, int col, bool useDefaultValueIfPossible) { KDbRecordData *currentRecord = record < int(m_data->count()) ? m_data->at(record) : m_insertRecord; //qDebug() << m_insertItem << m_currentRecord << currentRecord; if (m_recordEditing >= 0 && record == m_recordEditing && m_data->recordEditBuffer()) { KDbTableViewColumn* tvcol = column(col); if (tvcol->isDBAware()) { //get the stored value const int realFieldNumber = fieldNumberForColumn(col); if (realFieldNumber < 0) { qWarning() << "fieldNumberForColumn(m_curColumn) < 0"; return 0; } const QVariant *storedValue = ¤tRecord->at(realFieldNumber); //db-aware data: now, try to find a buffered value (or default one) const QVariant *cv = m_data->recordEditBuffer()->at(tvcol->columnInfo(), storedValue->isNull() && useDefaultValueIfPossible); if (cv) return cv; return storedValue; } //not db-aware data: const QVariant *cv = m_data->recordEditBuffer()->at(tvcol->field()->name()); if (cv) return cv; } //not db-aware data: const int realFieldNumber = fieldNumberForColumn(col); if (realFieldNumber < 0) { qWarning() << "fieldNumberForColumn(m_curColumn) < 0"; return 0; } return ¤tRecord->at(realFieldNumber); } void KexiDataAwareObjectInterface::startEditOrToggleValue() { if (!isReadOnly() && columnEditable(m_curColumn)) { if (columnType(m_curColumn) == KDbField::Boolean) { boolToggled(); } else { startEditCurrentCell(); return; } } } void KexiDataAwareObjectInterface::boolToggled() { startEditCurrentCell(); if (m_editor) { m_editor->clickedOnContents(); } acceptEditor(); updateCell(m_curRecord, m_curColumn); } void KexiDataAwareObjectInterface::slotDataDestroying() { m_data = 0; m_itemIterator = KDbTableViewDataIterator(); } void KexiDataAwareObjectInterface::addNewRecordRequested() { if (!isInsertingEnabled()) return; if (m_recordEditing >= 0) { if (!acceptRecordEditing()) return; } if (!hasData()) return; // find first column that is not autoincrement int columnToSelect = 0; int i = 0; foreach(KDbTableViewColumn *col, *data()->columns()) { if (!col->field()->isAutoIncrement()) { columnToSelect = i; break; } ++i; } CreateEditorFlags flags = DefaultCreateEditorFlags; flags ^= EnsureCellVisible; const int recordToAdd = recordCount(); createEditor(recordToAdd, columnToSelect, QString(), flags); if (m_editor) m_editor->setFocus(); const bool orig_acceptRecordEditing_in_setCursorPosition_enabled = m_acceptRecordEditing_in_setCursorPosition_enabled; m_acceptRecordEditing_in_setCursorPosition_enabled = false; setCursorPosition(recordToAdd, columnToSelect); m_acceptRecordEditing_in_setCursorPosition_enabled = orig_acceptRecordEditing_in_setCursorPosition_enabled; } bool KexiDataAwareObjectInterface::handleKeyPress(QKeyEvent *e, int *currentRecord, int *currentColumn, bool fullRecordSelection, bool *moveToFirstField, bool *moveToLastField) { Q_ASSERT(currentRecord); Q_ASSERT(currentColumn); if (moveToFirstField) *moveToFirstField = false; if (moveToLastField) *moveToLastField = false; const bool nobtn = e->modifiers() == Qt::NoModifier; const int k = e->key(); if ( (k == Qt::Key_Up) || (k == Qt::Key_PageUp && e->modifiers() == Qt::ControlModifier)) { selectPreviousRecord(); e->accept(); } else if ( (k == Qt::Key_Down) || (k == Qt::Key_PageDown && e->modifiers() == Qt::ControlModifier)) { selectNextRecord(); e->accept(); } else if (k == Qt::Key_PageUp && nobtn) { selectPreviousPage(); e->accept(); } else if (k == Qt::Key_PageDown && nobtn) { selectNextPage(); e->accept(); } else if (k == Qt::Key_Home) { if (fullRecordSelection) { //we're in record-selection mode: home key always moves to 1st record currentRecord = 0;//to 1st record } else {//cell selection mode: different actions depending on ctrl and shift keys mods if (nobtn) { *currentColumn = 0;//to 1st col } else if (e->modifiers() == Qt::ControlModifier) { *currentRecord = 0;//to 1st record and column *currentColumn = 0; } else return false; } if (moveToFirstField) *moveToFirstField = true; //do not accept yet e->ignore(); } else if (k == Qt::Key_End) { if (fullRecordSelection) { //we're in record-selection mode: home key always moves to the last record *currentRecord = m_data->count() - 1 + (isInsertingEnabled() ? 1 : 0);//to last the record } else {//cell selection mode: different actions depending on ctrl and shift keys mods if (nobtn) { *currentColumn = columnCount() - 1;//to last col } else if (e->modifiers() == Qt::ControlModifier) { *currentRecord = m_data->count() - 1 /*+(isInsertingEnabled()?1:0)*/; //to the last record and col *currentColumn = columnCount() - 1;//to last col } else return false; } if (moveToLastField) *moveToLastField = true; //do not accept yet e->ignore(); } else if (isInsertingEnabled() && ( (e->modifiers() == Qt::ControlModifier && k == Qt::Key_Equal) || (e->modifiers() == (Qt::ControlModifier | Qt::ShiftModifier) && k == Qt::Key_Equal) ) ) { *currentRecord = m_data->count(); //to the new record *currentColumn = 0;//to first col if (moveToFirstField) *moveToFirstField = true; //do not accept yet e->ignore(); } else { return false; } return true; } void KexiDataAwareObjectInterface::verticalScrollBarValueChanged(int v) { Q_UNUSED(v); if (!m_verticalScrollBarValueChanged_enabled) return; if (m_scrollbarToolTipsEnabled && verticalScrollBar()->isSliderDown()) { QWidget* thisWidget = dynamic_cast(this); const int record = lastVisibleRecord() + 1; if (thisWidget && record > 0) { const QString toolTipText( xi18n("Record: %1", record) ); QToolTip::showText( QPoint( verticalScrollBar()->mapToGlobal(QPoint(0, 0)).x() //thisWidget->mapToGlobal(verticalScrollBar()->pos()).x() - thisWidget->fontMetrics().width(toolTipText+" "), QCursor::pos().y() - thisWidget->fontMetrics().height() / 2 - thisWidget->fontMetrics().height() // because: "shown with a platform specific offset from the point of interest" ), toolTipText, 0, QRect() ); } } } void KexiDataAwareObjectInterface::setContextMenuTitle(const QIcon &icon, const QString &text) { m_contextMenuTitleIcon = icon; m_contextMenuTitleText = text; /*emit*/ reloadActions(); } bool KexiDataAwareObjectInterface::scrollbarToolTipsEnabled() const { return m_scrollbarToolTipsEnabled; } void KexiDataAwareObjectInterface::setScrollbarToolTipsEnabled(bool set) { m_scrollbarToolTipsEnabled = set; } void KexiDataAwareObjectInterface::focusOutEvent(QFocusEvent* e) { Q_UNUSED(e); updateCell(m_curRecord, m_curColumn); } int KexiDataAwareObjectInterface::showErrorMessageForResult(const KDbResultInfo& resultInfo) { QWidget *thisWidget = dynamic_cast(this); if (resultInfo.allowToDiscardChanges) { return KMessageBox::questionYesNo(thisWidget, resultInfo.message + (resultInfo.description.isEmpty() ? QString() : ("\n" + resultInfo.description)), QString(), KGuiItem(xi18nc("@action:button Correct Changes", "Correct"), QString(), xi18n("Correct changes")), KGuiItem(xi18nc("@action:button", "Discard Changes"))); } if (resultInfo.description.isEmpty()) { KMessageBox::sorry(thisWidget, resultInfo.message); } else { KMessageBox::detailedSorry(thisWidget, resultInfo.message, resultInfo.description); } return KMessageBox::Ok; } void KexiDataAwareObjectInterface::updateIndicesForVisibleValues() { m_indicesForVisibleValues.resize(m_data ? m_data->columnCount() : 0); if (!m_data) return; for (int i = 0; i < m_data->columnCount(); i++) { KDbTableViewColumn* tvCol = m_data->column(i); if (tvCol->columnInfo() && tvCol->columnInfo()->indexForVisibleLookupValue() != -1) // retrieve visible value from lookup field m_indicesForVisibleValues[ i ] = tvCol->columnInfo()->indexForVisibleLookupValue(); else m_indicesForVisibleValues[ i ] = i; } } /*! Performs searching \a stringValue in \a where string. \a matchAnyPartOfField, \a matchWholeField, \a wholeWordsOnly options are used to control how to search. If \a matchWholeField is true, \a wholeWordsOnly is not checked. \a firstCharacter is in/out parameter. If \a matchAnyPartOfField is true and \a matchWholeField is false, \a firstCharacter >= 0, the search will be performed after skipping first \a firstCharacter characters. If \a forward is false, we are searching backward from \a firstCharacter position. \a firstCharacter == -1 means then the last character. \a firstCharacter == INT_MAX means "before first" place, so searching fails immediately. On success, true is returned and \a firstCharacter is set to position of the matched string. */ static inline bool findInString(const QString& stringValue, int stringLength, const QString& where, int& firstCharacter, bool matchAnyPartOfField, bool matchWholeField, Qt::CaseSensitivity caseSensitivity, bool wholeWordsOnly, bool forward) { if (where.isEmpty()) { firstCharacter = -1; return false; } if (matchAnyPartOfField) { if (forward) { int pos = firstCharacter == -1 ? 0 : firstCharacter; if (wholeWordsOnly) { const int whereLength = where.length(); while (true) { pos = where.indexOf(stringValue, pos, caseSensitivity); if (pos == -1) break; if ( (pos > 0 && where.at(pos - 1).isLetterOrNumber()) || ((pos + stringLength - 1) < (whereLength - 1) && where.at(pos + stringLength - 1 + 1).isLetterOrNumber())) { pos++; // invalid match because before or after the string there is non-white space } else break; }//while firstCharacter = pos; } else {// !wholeWordsOnly firstCharacter = where.indexOf(stringValue, pos, caseSensitivity); } return firstCharacter != -1; } else { // !matchAnyPartOfField if (firstCharacter == INT_MAX) { firstCharacter = -1; //next time we'll be looking at different cell return false; } int pos = firstCharacter; if (wholeWordsOnly) { const int whereLength = where.length(); while (true) { pos = where.lastIndexOf(stringValue, pos, caseSensitivity); if (pos == -1) break; if ( (pos > 0 && where.at(pos - 1).isLetterOrNumber()) || ((pos + stringLength - 1) < (whereLength - 1) && where.at(pos + stringLength - 1 + 1).isLetterOrNumber())) { // invalid match because before or after the string there is non-white space pos--; if (pos < 0) // it can make pos < 0 break; } else break; }//while firstCharacter = pos; } else {// !wholeWordsOnly firstCharacter = where.lastIndexOf(stringValue, pos, caseSensitivity); } return firstCharacter != -1; } } else if (matchWholeField) { if (firstCharacter != -1 && firstCharacter != 0) { //we're not at 0-th char firstCharacter = -1; } else if ((caseSensitivity == Qt::CaseSensitive ? where : where.toLower()) == stringValue) { firstCharacter = 0; return true; } } else {// matchStartOfField if (firstCharacter != -1 && firstCharacter != 0) { //we're not at 0-th char firstCharacter = -1; } else if (where.startsWith(stringValue, caseSensitivity)) { if (wholeWordsOnly) { // If where.length() < stringValue.length(), true will be returned too - fine. return !where.at(stringValue.length()).isLetterOrNumber(); } firstCharacter = 0; return true; } } return false; } tristate KexiDataAwareObjectInterface::find(const QVariant& valueToFind, const KexiSearchAndReplaceViewInterface::Options& options, bool next) { if (!hasData()) return cancelled; const QVariant prevSearchedValue(m_recentlySearchedValue); m_recentlySearchedValue = valueToFind; const KexiSearchAndReplaceViewInterface::Options::SearchDirection prevSearchDirection = m_recentSearchDirection; m_recentSearchDirection = options.searchDirection; if (valueToFind.isNull() || valueToFind.toString().isEmpty()) return cancelled; const bool forward = (options.searchDirection == KexiSearchAndReplaceViewInterface::Options::SearchUp) ? !next : next; //direction can be reversed if ((!prevSearchedValue.isNull() && prevSearchedValue != valueToFind) || (prevSearchDirection != options.searchDirection && options.searchDirection == KexiSearchAndReplaceViewInterface::Options::SearchAllRecords)) { // restart searching when value has been changed or new direction is SearchAllRecords m_positionOfRecentlyFoundValue.exists = false; } const bool startFrom1stRecordAndCol = !m_positionOfRecentlyFoundValue.exists && next && options.searchDirection == KexiSearchAndReplaceViewInterface::Options::SearchAllRecords; const bool startFromLastRecordAndCol = ( !m_positionOfRecentlyFoundValue.exists && !next && options.searchDirection == KexiSearchAndReplaceViewInterface::Options::SearchAllRecords) || (m_curRecord >= recordCount() && !forward); //we're at "insert" record, and searching backwards: move to the last cell if (!startFrom1stRecordAndCol && !startFromLastRecordAndCol && m_curRecord >= recordCount()) { //we're at "insert" record, and searching forward: no chances to find something return false; } KDbTableViewDataIterator it((startFrom1stRecordAndCol || startFromLastRecordAndCol) ? m_data->begin() : m_itemIterator /*start from the current cell*/); if (startFromLastRecordAndCol) it += (m_data->columnCount() - 1); int firstCharacter; if (m_positionOfRecentlyFoundValue.exists) {// start after the next/prev char position if (forward) firstCharacter = m_positionOfRecentlyFoundValue.lastCharacter + 1; else { firstCharacter = (m_positionOfRecentlyFoundValue.firstCharacter > 0) ? (m_positionOfRecentlyFoundValue.firstCharacter - 1) : INT_MAX /* this means 'before first'*/; } } else { firstCharacter = -1; //forward ? -1 : INT_MAX; } const int columnCount = m_data->columnCount(); int record, column; if (startFrom1stRecordAndCol) { record = 0; column = 0; } else if (startFromLastRecordAndCol) { record = recordCount() - 1; column = columnCount - 1; } else { record = m_curRecord; column = m_curColumn; } //sache some flags for efficiency const bool matchAnyPartOfField = options.textMatching == KexiSearchAndReplaceViewInterface::Options::MatchAnyPartOfField; const bool matchWholeField = options.textMatching == KexiSearchAndReplaceViewInterface::Options::MatchWholeField; const Qt::CaseSensitivity caseSensitivity = options.caseSensitive ? Qt::CaseSensitive : Qt::CaseInsensitive; const bool wholeWordsOnly = options.wholeWordsOnly; int columnNumber = (options.columnNumber == KexiSearchAndReplaceViewInterface::Options::CurrentColumn) ? m_curColumn : options.columnNumber; if (columnNumber >= 0) column = columnNumber; const bool lookInAllColumns = columnNumber == KexiSearchAndReplaceViewInterface::Options::AllColumns; int firstColumn; // real number of the first column, can be smaller than lastColumn if forward==true int lastColumn; // real number of the last column if (lookInAllColumns) { firstColumn = forward ? 0 : columnCount - 1; lastColumn = forward ? columnCount - 1 : 0; } else { firstColumn = columnNumber; lastColumn = columnNumber; } const QString stringValue( caseSensitivity == Qt::CaseSensitive ? valueToFind.toString() : valueToFind.toString().toLower()); const int stringLength = stringValue.length(); // search const int prevRecord = m_curRecord; KDbRecordData *data = 0; while ((it != m_data->end() && (data = *it))) { for (; forward ? column <= lastColumn : column >= lastColumn; column = forward ? (column + 1) : (column - 1)) { const QVariant cell(data->at(m_indicesForVisibleValues[ column ])); if (findInString(stringValue, stringLength, cell.toString(), firstCharacter, matchAnyPartOfField, matchWholeField, caseSensitivity, wholeWordsOnly, forward)) { setCursorPosition(record, column, ForceSetCursorPosition); if (prevRecord != m_curRecord) updateRecord(prevRecord); // remember the exact position for the found value m_positionOfRecentlyFoundValue.exists = true; m_positionOfRecentlyFoundValue.firstCharacter = firstCharacter; //! @todo for regexp lastCharacter should be computed m_positionOfRecentlyFoundValue.lastCharacter = firstCharacter + stringLength - 1; return true; } }//for if (forward) { ++it; ++record; } else { if (m_data->begin() == it) { break; } else { --it; --record; } } column = firstColumn; }//while return false; } tristate KexiDataAwareObjectInterface::findNextAndReplace( const QVariant& valueToFind, const QVariant& replacement, const KexiSearchAndReplaceViewInterface::Options& options, bool replaceAll) { Q_UNUSED(replacement); Q_UNUSED(options); Q_UNUSED(replaceAll); if (isReadOnly()) return cancelled; if (valueToFind.isNull() || valueToFind.toString().isEmpty()) return cancelled; //! @todo implement KexiDataAwareObjectInterface::findAndReplace() return false; } void KexiDataAwareObjectInterface::setRecordEditing(int record) { if (record == m_recordEditing) { return; } if (m_recordEditing >= 0 && record >= 0) { qWarning() << "Cannot set editing for row" << record << "before editing of row" << m_recordEditing << "is accepted or cancelled"; return; } m_recordEditing = record; if (record >= 0) { emit recordEditingStarted(record); } else { emit recordEditingTerminated(record); } } void KexiDataAwareObjectInterface::showEditorContextMessage( KexiDataItemInterface *item, const QString &message, KMessageWidget::MessageType type, KMessageWidget::CalloutPointerDirection direction) { QWidget *par = dynamic_cast(this) ? dynamic_cast(this)->widget() : dynamic_cast(this); QWidget *edit = dynamic_cast(item); if (par && edit) { delete m_errorMessagePopup; KexiContextMessage msg(message); m_errorMessagePopup = new KexiContextMessageWidget(dynamic_cast(this), 0, 0, msg); QPoint arrowPos = par->mapToGlobal(edit->pos()) + QPoint(12, edit->height() + 6); if (verticalHeader()) { arrowPos += QPoint(verticalHeader()->width(), horizontalHeaderHeight()); } m_errorMessagePopup->setMessageType(type); m_errorMessagePopup->setCalloutPointerDirection(direction); m_errorMessagePopup->setCalloutPointerPosition(arrowPos); m_errorMessagePopup->setWordWrap(false); m_errorMessagePopup->setClickClosesMessage(true); m_errorMessagePopup->resizeToContents(); QObject::connect(m_errorMessagePopup, SIGNAL(animatedHideFinished()), edit, SLOT(setFocus())); m_errorMessagePopup->animatedShow(); edit->setFocus(); } } static QString lengthExceededMessage(KexiDataItemInterface *item) { return xi18np( "Limit of %2 characters for %3 field has been exceeded by %1 character.\n" "Fix the text or it will be truncated upon saving changes.", "Limit of %2 characters for %3 field has been exceeded by %1 characters.\n" "Fix the text or it will be truncated upon saving changes.", item->value().toString().length() - item->columnInfo()->field()->maxLength(), item->columnInfo()->field()->maxLength(), item->columnInfo()->captionOrAliasOrName()); } void KexiDataAwareObjectInterface::showLengthExceededMessage(KexiDataItemInterface *item, bool exceeded) { if (exceeded) { if (item) { showEditorContextMessage( item, lengthExceededMessage(item), KMessageWidget::Warning, KMessageWidget::Up); m_lengthExceededMessageVisible = true; } } else { if (m_errorMessagePopup) { m_errorMessagePopup->animatedHide(); m_lengthExceededMessageVisible = false; } } } void KexiDataAwareObjectInterface::showUpdateForLengthExceededMessage(KexiDataItemInterface *item) { if (m_errorMessagePopup && m_lengthExceededMessageVisible) { m_errorMessagePopup->setText(lengthExceededMessage(item)); m_errorMessagePopup->resizeToContents(); } } diff --git a/src/widget/dataviewcommon/kexidataawareobjectiface.h b/src/widget/dataviewcommon/kexidataawareobjectiface.h index 190d3d766..90923adc4 100644 --- a/src/widget/dataviewcommon/kexidataawareobjectiface.h +++ b/src/widget/dataviewcommon/kexidataawareobjectiface.h @@ -1,1040 +1,1041 @@ /* This file is part of the KDE project Copyright (C) 2005-2015 Jarosław Staniek Based on KexiTableView code. Copyright (C) 2002 Till Busch Copyright (C) 2003 Lucijan Busch Copyright (C) 2003 Daniel Molkentin Copyright (C) 2003 Joseph Wenninger 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. */ #ifndef KEXIDATAAWAREOBJECTINTERFACE_H #define KEXIDATAAWAREOBJECTINTERFACE_H #include "kexidataviewcommon_export.h" #include #include #include #include #include #include #include #include class QHeaderView; class QScrollBar; class QMenu; class KDbRecordData; class KDbTableViewData; class KexiRecordNavigatorIface; //! default column width in pixels #define KEXI_DEFAULT_DATA_COLUMN_WIDTH 120 //! \brief The KexiDataAwareObjectInterface is an interface for record-based data object. /** This interface is implemented by KexiTableScrollArea and KexiFormView and used by KexiDataAwareView. If yu're implementing this interface, add KEXI_DATAAWAREOBJECTINTERFACE convenience macro just after Q_OBJECT. You should add following code to your destructor so data is deleted: \code if (m_owner) delete m_data; m_data = 0; \endcode This is not performed in KexiDataAwareObjectInterface because you may need to access m_data in your desctructor. */ class KEXIDATAVIEWCOMMON_EXPORT KexiDataAwareObjectInterface { public: KexiDataAwareObjectInterface(); virtual ~KexiDataAwareObjectInterface(); /*! Sets data for this object. if \a owner is true, the object will own \a data and therefore will be destroyed when needed, else: \a data is (possibly) shared and not owned by the widget. If widget already has _different_ data object assigned (and owns this data), old data is destroyed before new assignment. */ void setData(KDbTableViewData *data, bool owner = true); /*! \return data structure displayed for this object */ inline KDbTableViewData *data() const { return m_data; } /*! \return currently selected column number or -1. */ inline int currentColumn() const { return m_curColumn; } /*! \return number of the currently selected record number or -1. */ inline int currentRecord() const { return m_curRecord; } /*! \return last record visible on the screen (counting from 0). The returned value is guaranteed to be smaller or equal to currentRecord() or -1 if there are no records. */ virtual int lastVisibleRecord() const = 0; /*! \return currently selected record data or null. */ KDbRecordData *selectedRecord() const { return m_currentRecord; } /*! \return number of records in this view. */ int recordCount() const; /*! \return number of visible columns in this view. By default returns dataColumns(), what is proper table view. In case of form view, there can be a number of duplicated columns defined (data-aware widgets, see KexiFormScrollView::columnCount()), so columnCount() can return greater number than dataColumns(). */ virtual int columnCount() const; /*! Helper function. \return number of columns of data. */ int dataColumns() const; /*! \return true if data represented by this object is not editable (it can be editable with other ways although, outside of this object). */ virtual bool isReadOnly() const; /*! Sets readOnly flag for this object. Unless the flag is set, the widget inherits readOnly flag from it's data structure assigned with setData(). The default value if false. This method is useful when you need to switch on the flag indepentently from the data structure. Note: it is not allowed to force readOnly off when internal data is readOnly - in that case the method does nothing. You can check internal data flag calling data()->isReadOnly(). If \a set is true, insertingEnabled flag will be cleared automatically. \sa isInsertingEnabled() */ void setReadOnly(bool set); /*! \return true if sorting is enabled. */ inline bool isSortingEnabled() const { return m_isSortingEnabled; } /*! Sets sorting order on column @a column to @a order. This method do not work if sorting is disabled using setSortingEnabled(false). @a column may be -1, what means "no sorting". */ - virtual void setSorting(int column, Qt::SortOrder order = Qt::AscendingOrder); + virtual void setSorting(int column, + KDbOrderByColumn::SortOrder order = KDbOrderByColumn::SortOrder::Ascending); /*! Enables or disables sorting for this object This method is different that setSorting() because it prevents both user and programmer from sorting by clicking a column's header or calling setSorting(). By default sorting is enabled. */ virtual void setSortingEnabled(bool set); /*! \return sorted column number or -1 if no column is sorted within data or there is no data assigned at all. This does not mean that any sorting has been performed within GUI of this object, because the data could be changed in the meantime outside of this GUI object. */ int dataSortColumn() const; /*! \return sort order for data. This information does not mean that any sorting has been performed within GUI of this object, because the data could be changed in the meantime outside of this GUI object. dataSortColumn() should be checked first to see if sorting is enabled (and if there's data). */ - Qt::SortOrder dataSortOrder() const; + KDbOrderByColumn::SortOrder dataSortOrder() const; /*! Sorts all records by column selected with setSorting(). If there is currently record edited, it is accepted. If acception failed, sort() will return false. \return true on success. */ virtual bool sort(); /*! Sorts currently selected column in ascending order. This slot is used typically for "data_sort_az" action. */ void sortAscending(); /*! Sorts currently selected column in descending order. This slot is used typically for "data_sort_za" action. */ void sortDescending(); /*! \return true if data inserting is enabled (the default). */ virtual bool isInsertingEnabled() const; /*! Sets insertingEnabled flag. If true, empty record is available at the end of this widget for new entering new data. Unless the flag is set, the widget inherits insertingEnabled flag from it's data structure assigned with setData(). The default value if false. Note: it is not allowed to force insertingEnabled on when internal data has insertingEnabled set off - in that case the method does nothing. You can check internal data flag calling data()->insertingEnabled(). Setting this flag to true will have no effect if read-only flag is true. \sa setReadOnly() */ void setInsertingEnabled(bool set); /*! \return true if record deleting is enabled. Equal to deletionPolicy() != NoDelete && !isReadOnly()). */ bool isDeleteEnabled() const; /*! \return true if inserting empty records is enabled (false by default). Mostly usable for not db-aware objects (e.g. used in Kexi Alter Table). Note, that if inserting is disabled, or the data set is read-only, this flag will be ignored. */ bool isEmptyRecordInsertingEnabled() const { return m_emptyRecordInsertingEnabled; } /*! Sets the emptyRecordInsertingEnabled flag. Note, that if inserting is disabled, this flag is ignored. */ void setEmptyRecordInsertingEnabled(bool set); /*! Enables or disables filtering. Filtering is enabled by default. */ virtual void setFilteringEnabled(bool set); /*! \return true if filtering is enabled. */ inline bool isFilteringEnabled() const { return m_isFilteringEnabled; } /*! Added for convenience: if @a set is true, this object will behave more like a spreadsheet (it's used for things like table designer view): - hides navigator - disables sorting, inserting and filtering - enables accepting record after cell accepting; see setAcceptsRecordEditAfterCellAccepting() - enables inserting empty record; see setEmptyRecordInsertingEnabled() */ virtual void setSpreadSheetMode(bool set); /*! \return true id "spreadSheetMode" is enabled. It's false by default. */ bool spreadSheetMode() const { return m_spreadSheetMode; } /*! \return number of currently edited record or -1. */ inline int recordEditing() const { return m_recordEditing; } enum DeletionPolicy { NoDelete = 0, AskDelete = 1, ImmediateDelete = 2, SignalDelete = 3 }; /*! \return deletion policy for this object. The default (after allocating) is AskDelete. */ DeletionPolicy deletionPolicy() const { return m_deletionPolicy; } virtual void setDeletionPolicy(DeletionPolicy policy); /*! Deletes currently selected record; does nothing if no record is currently selected. If record is in edit mode, editing is cancelled before deleting. */ virtual void deleteCurrentRecord(); /*! Inserts one empty record above \a pos. If \a pos is -1 (the default), new record is inserted above the current record (or above 1st record if there is no current). A new record becomes current if \a pos is -1 or if \a pos is equal currentRecord(). This method does nothing if: -inserting flag is disabled (see isInsertingEnabled()) -read-only flag is set (see isReadOnly()) \return inserted record's data */ virtual KDbRecordData *insertEmptyRecord(int pos = -1); /*! For reimplementation: called by deleteItem(). If returns false, deleting is aborted. Default implementation just returns true. */ virtual bool beforeDeleteItem(KDbRecordData *data); /*! Deletes \a record. Used by deleteCurrentRecord(). Calls beforeDeleteItem() before deleting, to double-check if deleting is allowed. \return true on success. */ bool deleteItem(KDbRecordData *data); /*! Inserts \a data at position \a pos. -1 means current record. Used by insertEmptyRecord(). */ void insertItem(KDbRecordData *data, int pos = -1); /*! Clears entire table data, its visible representation and deletes data at database backend (if this is db-aware object). Does not clear columns information. Does not destroy KDbTableViewData object (if present) but only clears its contents. Displays confirmation dialog if \a ask is true (the default is false). Repaints widget if \a repaint is true (the default). For empty tables, true is returned immediately. If isDeleteEnabled() is false, false is returned. For spreadsheet mode all current records are just replaced by empty records. \return true on success, false on failure, and cancelled if user cancelled deletion (only possible if \a ask is true). */ tristate deleteAllRecords(bool ask = false, bool repaint = true); /*! \return maximum number of records that can be displayed per one "page" for current view's size. */ virtual int recordsPerPage() const = 0; virtual void selectRecord(int record); virtual void selectNextRecord(); virtual void selectPreviousRecord(); virtual void selectNextPage(); //!< page down action virtual void selectPreviousPage(); //!< page up action virtual void selectFirstRecord(); virtual void selectLastRecord(); virtual void addNewRecordRequested(); /*! Clears the current selection. Current record and column will be now unspecified: currentRecord(), currentColumn() will return -1, and selectedRecord() will return null. */ virtual void clearSelection(); //! Flags for setCursorPosition() enum CursorPositionFlag { NoCursorPositionFlags = 0, //!< Default flag ForceSetCursorPosition = 1, //!< Update cursor position even if record and col doesn't //!< differ from actual position. DontEnsureCursorVisibleIfPositionUnchanged = 2 //!< Don't call ensureCellVisible() //!< when position is unchanged and //!< ForceSetCursorPosition is off. }; Q_DECLARE_FLAGS(CursorPositionFlags, CursorPositionFlag) /*! Moves cursor to \a record and \a col. If \a col is -1, current column number is used. If forceSet is true, cursor position is updated even if \a record and \a col doesn't differ from actual position. */ virtual void setCursorPosition(int record, int col = -1, CursorPositionFlags flags = NoCursorPositionFlags); /*! Ensures that cell at \a record and \a col is visible. If \a col is -1, current column number is used. \a record and \a col, if not -1, must be between 0 and recordCount()-1 (or columnCount()-1 accordingly). */ virtual void ensureCellVisible(int record, int col) = 0; /*! Ensures that column \a col is visible. If \a col is -1, current column number is used. \a col, if not -1, must be between 0 and columnCount()-1. */ virtual void ensureColumnVisible(int col) = 0; /*! Specifies, if this object automatically accepts record editing (using acceptRecordEdit()) on accepting any cell's edit (i.e. after acceptEditor()). \sa acceptsRecordEditAfterCellAccepting() */ virtual void setAcceptsRecordEditAfterCellAccepting(bool set); /*! \return true, if this object automatically accepts record editing (using acceptRecordEdit()) on accepting any cell's edit (i.e. after acceptEditor()). By default this flag is set to false. Not that if the query for this table has given constraints defined, like NOT NULL / NOT EMPTY for more than one field - editing a record would be impossible for the flag set to true, because of constraints violation. However, setting this flag to true can be useful especially for not-db-aware data set (it's used e.g. in Kexi Alter Table's field editor). */ bool acceptsRecordEditAfterCellAccepting() const { return m_acceptsRecordEditAfterCellAccepting; } /*! \return true, if this table accepts dropping data on the records. */ bool dropsAtRecordEnabled() const { return m_dropsAtRecordEnabled; } /*! Specifies, if this table accepts dropping data on the records. If enabled: - dragging over record is indicated by drawing a line at bottom side of this record - dragOverRecord() signal will be emitted on dragging, -droppedAtRecord() will be emitted on dropping By default this flag is set to false. */ virtual void setDropsAtRecordEnabled(bool set); /*! \return currently used data (field/cell) editor or 0 if there is no data editing. */ inline KexiDataItemInterface *editor() const { return m_editor; } /*! Cancels record editing. All changes made to the editing record during this current session will be undone. \return true on success or false on failure (e.g. when editor does not exist) */ virtual bool cancelRecordEditing(); /*! Accepts record editing. All changes made to the editing record during this current session will be accepted (saved). \return true if accepting was successful, false otherwise (e.g. when current record contains data that does not meet given constraints). */ virtual bool acceptRecordEditing(); virtual void removeEditor(); /*! Cancels changes made to the currently active editor. Reverts the editor's value to old one. \return true on success or false on failure (e.g. when editor does not exist) */ virtual bool cancelEditor(); //! Accepst changes made to the currently active editor. //! \return true on success or false on failure (e.g. when editor does not exist or there is data validation error) virtual bool acceptEditor(); //! Flags for use in createEditor() enum CreateEditorFlag { ReplaceOldValue = 1, //!< Remove old value replacing it with a new one EnsureCellVisible = 2, //!< Ensure the cell behind the editor is visible DefaultCreateEditorFlags = EnsureCellVisible //!< Default flags. }; Q_DECLARE_FLAGS(CreateEditorFlags, CreateEditorFlag) //! Creates editors and shows it, what usually means the beginning of a cell editing virtual void createEditor(int record, int col, const QString& addText = QString(), CreateEditorFlags flags = DefaultCreateEditorFlags) = 0; /*! Used when Return key is pressed on cell, the cell has been double clicked or "+" navigator's button is clicked. Also used when we want to continue editing a cell after "invalid value" message was displayed (in this case, \a setText is usually not empty, what means that text will be set in the cell replacing previous value). */ virtual void startEditCurrentCell(const QString& setText = QString(), CreateEditorFlags flags = DefaultCreateEditorFlags); /*! Deletes currently selected cell's contents, if allowed. In most cases delete is not accepted immediately but "record editing" mode is just started. */ virtual void deleteAndStartEditCurrentCell(); inline KDbRecordData *recordAt(int pos) const; /*! \return column information for column number \a col. Default implementation just returns column # col, but for Kexi Forms column data corresponding to widget number is used here (see KexiFormScrollView::fieldNumberForColumn()). */ virtual KDbTableViewColumn* column(int col); /*! \return field number within data model connected to a data-aware widget at column \a col. Can return -1 if there's no such column. */ virtual int fieldNumberForColumn(int col) { return col; } bool hasDefaultValueAt(const KDbTableViewColumn& tvcol); const QVariant* bufferedValueAt(int record, int col, bool useDefaultValueIfPossible = true); //! \return a type of column \a col - one of KDbField::Type int columnType(int col); //! \return default value for column \a col QVariant columnDefaultValue(int col) const; /*! \return true if column \a col is editable. Default implementation takes information about 'readOnly' flag from data member. Within forms, this is reimplemented for checking 'readOnly' flag from a widget ('readOnly' flag from data member is still checked though). */ virtual bool columnEditable(int col); /*! Redraws the current cell. To be implemented. */ virtual void updateCurrentCell() = 0; //! @return height of the horizontal header, 0 by default. virtual int horizontalHeaderHeight() const; //! signals virtual void itemChanged(KDbRecordData* data, int record, int column) = 0; virtual void itemChanged(KDbRecordData* data, int record, int column, const QVariant &oldValue) = 0; virtual void itemDeleteRequest(KDbRecordData* data, int record, int column) = 0; virtual void currentItemDeleteRequest() = 0; //! Emitted for spreadsheet mode when an item was deleted and a new item has been appended virtual void newItemAppendedForAfterDeletingInSpreadSheetMode() = 0; /*! Data has been refreshed on-screen - emitted from initDataContents(). */ virtual void dataRefreshed() = 0; virtual void dataSet(KDbTableViewData *data) = 0; /*! \return a pointer to context menu. This can be used to plug some actions there. */ QMenu* contextMenu() const { return m_contextMenu; } /*! \return true if the context menu is enabled (visible) for the view. True by default. */ bool contextMenuEnabled() const { return m_contextMenuEnabled; } /*! Enables or disables the context menu for the view. */ void setContextMenuEnabled(bool set) { m_contextMenuEnabled = set; } /*! Sets a title with icon for the context menu. Set empty icon and text to remove the title item. This method should be called before customizing the menu because it will be recreated by the method. */ void setContextMenuTitle(const QIcon &icon, const QString &text); /*! \return title text of the context menu. */ QString contextMenuTitleText() const { return m_contextMenuTitleText; } /*! \return title icon of the context menu. */ QIcon contextMenuTitleIcon() const { return m_contextMenuTitleIcon; } /*! \return true if vertical scrollbar's tooltips are enabled (true by default). */ bool scrollbarToolTipsEnabled() const; /*! Enables or disables vertical scrollbar's tooltip. */ void setScrollbarToolTipsEnabled(bool set); /*! Typically handles pressing Enter or F2 key: if current cell has boolean type, toggles it's value, otherwise starts editing (startEditCurrentCell()). */ void startEditOrToggleValue(); /*! \return true if the new record is edited; implies: recordEditing==true. */ inline bool newRecordEditing() const { return m_newRecordEditing; } /*! Reaction on toggling a boolean value of a cell: we're starting to edit the cell and inverting it's state. */ virtual void boolToggled(); virtual void connectCellSelectedSignal(const QObject* receiver, const char* intIntMember) = 0; virtual void connectRecordEditingStartedSignal(const QObject* receiver, const char* intMember) = 0; virtual void connectRecordEditingTerminatedSignal(const QObject* receiver, const char* voidMember) = 0; virtual void connectUpdateSaveCancelActionsSignal(const QObject* receiver, const char* voidMember) = 0; virtual void connectReloadActionsSignal(const QObject* receiver, const char* voidMember) = 0; virtual void connectDataSetSignal(const QObject* receiver, const char* kexiTableViewDataMember) = 0; virtual void connectToReloadDataSlot(const QObject* sender, const char* voidSignal) = 0; virtual void slotDataDestroying(); //! Copy current selection to a clipboard (e.g. cell) virtual void copySelection() = 0; //! Cut current selection to a clipboard (e.g. cell) virtual void cutSelection() = 0; //! Paste current clipboard contents (e.g. to a cell) virtual void paste() = 0; /*! Finds \a valueToFind within the data items \a options are used to control the process. Selection is moved to found value. If \a next is true, "find next" is performed, else "find previous" is performed. Searching behaviour also depends on status of the previous search: for every search, position of the cells containing the found value is stored internally by the data-aware interface (not in options). Moreover, position (start, end) of the found value is also stored. Thus, the subsequent search will reuse this information to be able to start searching exactly after the previously found value (or before for "find previous" option). The flags can be zeroed, what will lead to seaching from the first character of the current item (cell). \return true if value has been found, false if value has not been found, and cancelled if there is nothing to find or there is no data to search in. */ virtual tristate find(const QVariant& valueToFind, const KexiSearchAndReplaceViewInterface::Options& options, bool next); /*! Finds \a valueToFind within the data items and replaces with \a replacement \a options are used to control the process. \return true if value has been found and replaced, false if value has not been found and replaced, and cancelled if there is nothing to find or there is no data to search in or the data is read only. If \a replaceAll is true, all found values are replaced. */ virtual tristate findNextAndReplace(const QVariant& valueToFind, const QVariant& replacement, const KexiSearchAndReplaceViewInterface::Options& options, bool replaceAll); /*! \return vertical scrollbar */ virtual QScrollBar* verticalScrollBar() const = 0; /*! Used in KexiTableView::keyPressEvent() (and in continuous forms). \return true when the key press event \e was consumed. You should also check e->isAccepted(), if it's true, nothing should be done; if it is false, you should call setCursorPosition() for the altered \a curentColumn and \c currentRecord variables. If \a moveToFirstField is not 0, *moveToFirstField will be set to true when the cursor should be moved to the first field (in tab order) and to false otherwise. If \a moveToLastField is not 0, *moveToLastField will be set to true when the cursor should be moved to the last field (in tab order) and to false otherwise. Note for forms: if moveToFirstField and moveToLastField are not 0, \a currentColumn is altered after calling this method, so setCursorPosition() will set to the index of an appropriate column (field). This is needed because field widgets can be inserted and ordered in custom tab order, so the very first field in the data source can be other than the very first field in the form. Used by KexiTableView::keyPressEvent() and KexiTableView::keyPressEvent(). */ virtual bool handleKeyPress(QKeyEvent *e, int *currentRecord, int *currentColumn, bool fullRecordSelection, bool *moveToFirstField = 0, bool *moveToLastField = 0); protected: /*! Reimplementation for KexiDataAwareObjectInterface. Initializes data contents (resizes it, sets cursor at the first record). Sets record count for record navigator. Sets cursor positin (using setCursorPosition()) to first record or sets (-1, -1) position if no records are available. Called on setData(). Also called once on show event after refreshRequested() signal was received from TableViewData object. */ virtual void initDataContents(); /*! Clears columns information and thus all internal table data and its visible representation. Repaints widget if \a repaint is true. */ virtual void clearColumns(bool repaint = true); /*! Called by clearColumns() to clear internals of the object. For example, KexiTableView removes contents of it's horizontal header. */ virtual void clearColumnsInternal(bool repaint) = 0; /*! @internal for implementation \return sorting order (within GUI). currentLocalSortColumn() should be also checked, and if it returns -1, no particular sorting is set up. Even this does not mean that any sorting has been performed within GUI of this object, because the data could be changed in the meantime outside of this GUI object. @see dataSortOrder() currentLocalSortColumn() */ - virtual Qt::SortOrder currentLocalSortOrder() const = 0; + virtual KDbOrderByColumn::SortOrder currentLocalSortOrder() const = 0; /*! @internal for implementation \return sorted column number for this widget or -1 if no column is sorted witin GUI. This does not mean that the same sorting is performed within data member which is used by this widget, because the data could be changed in the meantime outside of this GUI widget. @see dataSortColumn() currentLocalSortOrder() */ virtual int currentLocalSortColumn() const = 0; /*! @internal for implementation Shows sorting indicator order in the GUI for column @a column. This should not perform any sorting in data assigned to this object. @a column may be -1, what means "no sorting". */ - virtual void setLocalSortOrder(int column, Qt::SortOrder order) = 0; + virtual void setLocalSortOrder(int column, KDbOrderByColumn::SortOrder order) = 0; /*! @internal Sets order for \a column: -1: descending, 1: ascending, 0: invert order */ virtual void sortColumnInternal(int col, int order = 0); /*! @internal for implementation Updates GUI after sorting. After sorting you need to ensure current record and column is visible to avoid user confusion. For exaple, in KexiTableView implementation, current cell is centered (if possible) and updateContents() is called. */ virtual void updateGUIAfterSorting(int previousRecord) = 0; /*! Emitted in initActions() to force reload actions You should remove existing actions and add them again. Define and emit reloadActions() signal here. */ virtual void reloadActions() = 0; /*! Reloads data for this object. */ virtual void reloadData(); /*! for implementation as a signal */ virtual void itemSelected(KDbRecordData *) = 0; /*! for implementation as a signal */ virtual void cellSelected(int record, int col) = 0; /*! for implementation as a signal */ virtual void sortedColumnChanged(int col) = 0; /*! for implementation as a signal */ virtual void recordEditingTerminated(int record) = 0; /*! for implementation as a signal */ virtual void updateSaveCancelActions() = 0; /*! Prototype for signal recordEditingStarted(int), implemented by KexiFormScrollView. */ virtual void recordEditingStarted(int record) = 0; /*! Clear temporary members like the pointer to current editor. If you reimplement this method, don't forget to call this one. */ virtual void clearVariables(); /*! @internal Creates editor structure without filling it with data. Used in createEditor() and few places to be able to display cell contents dependending on its type. If \a ignoreMissingEditor is false (the default), and editor cannot be instantiated, current record editing (if present) is cancelled. */ virtual KexiDataItemInterface *editor(int col, bool ignoreMissingEditor = false) = 0; /*! Updates editor's position, size and shows its focus (not the editor!) for \a record and \a column, using editor(). Does nothing if editor not found. */ virtual void editorShowFocus(int record, int column) = 0; /*! Redraws specified cell. */ virtual void updateCell(int record, int column) = 0; /*! Redraws all cells of specified record \a record. */ virtual void updateRecord(int record) = 0; /*! Updates contents of the widget. Just call update() here on your widget. */ virtual void updateWidgetContents() = 0; /*! Updates widget's contents size e.g. using QScrollView::resizeContents(). */ virtual void updateWidgetContentsSize() = 0; /*! @internal Updates record appearance after canceling record edit. Used by cancelRecordEdit(). By default just calls updateRecord(m_curRecord). Reimplemented by KexiFormScrollView. */ virtual void updateAfterCancelRecordEditing(); /*! @internal Updates record appearance after accepting record edit. Used by acceptRecordEditing(). By default just calls updateRecord(m_curRecord). Reimplemented by KexiFormScrollView. */ virtual void updateAfterAcceptRecordEditing(); //! Handles TableViewData::recordRepaintRequested() signal virtual void slotRecordRepaintRequested(KDbRecordData* data) { Q_UNUSED(data); } //! Handles TableViewData::aboutToDeleteRecord() signal. Prepares info for slotRecordDeleted(). virtual void slotAboutToDeleteRecord(KDbRecordData* data, KDbResultInfo* result, bool repaint); //! Handles TableViewData::recordDeleted() signal to repaint when needed. virtual void slotRecordDeleted(); //! Handles TableViewData::recordInserted() signal to repaint when needed. virtual void slotRecordInserted(KDbRecordData *data, bool repaint); virtual void beginInsertItem(KDbRecordData *data, int pos); virtual void endInsertItem(KDbRecordData *data, int pos); virtual void beginRemoveItem(KDbRecordData *data, int pos); virtual void endRemoveItem(int pos); //! Like above, not db-aware version virtual void slotRecordInserted(KDbRecordData *data, int record, bool repaint); virtual void slotRecordsDeleted(const QList &) {} //! for sanity checks (return true if m_data is present; else: outputs warning) inline bool hasData() const; /*! Used by setCursorPosition() if cursor's position changed. */ virtual void selectCellInternal(int previousRecord, int previousColumn); /*! Used in KexiDataAwareObjectInterface::slotRecordDeleted() to repaint tow \a record and all visible below. Implemented if there is more than one record displayed, i.e. currently for KexiTableView. */ virtual void updateAllVisibleRecordsBelow(int record) { Q_UNUSED(record); } /*! @return geometry of the viewport, i.e. the scrollable area, minus any scrollbars, etc. */ virtual QRect viewportGeometry() const = 0; //! Call this from the subclass. */ virtual void focusOutEvent(QFocusEvent* e); /*! Handles verticalScrollBar()'s valueChanged(int) signal. Called when vscrollbar's value has been changed. Call this method from the subclass. */ virtual void verticalScrollBarValueChanged(int v); /*! Changes 'record editing' >=0 there's currently edited record, else -1. * Can be reimplemented with calling superclass setRecordEditing() * Sends recordEditingStarted(int) signal. * @see recordEditing() recordEditingStarted(). */ void setRecordEditing(int record); /*! Shows error message box suitable for \a resultInfo. This can be "sorry" or "detailedSorry" message box or "queryYesNo" if resultInfo->allowToDiscardChanges is true. \return code of button clicked: KMessageBox::Ok in case of "sorry" or "detailedSorry" messages and KMessageBox::Yes or KMessageBox::No in case of "queryYesNo" message. */ int showErrorMessageForResult(const KDbResultInfo& resultInfo); /*! Shows context message @a message for editor @a item. */ void showEditorContextMessage( KexiDataItemInterface *item, const QString &message, KMessageWidget::MessageType type, KMessageWidget::CalloutPointerDirection direction); /*! Shows context message about exceeded length for editor @a item. If @a exceeded is true, a new message is created, else the message will be removed. */ void showLengthExceededMessage(KexiDataItemInterface *item, bool exceeded); /*! Updates message about exceeded length for editor @a item. Useful only where message created with showLengthExceededMessage() is displayed. */ void showUpdateForLengthExceededMessage(KexiDataItemInterface *item); /*! Prepares array of indices of visible values to search within. This is per-interface global cache. Needed for faster lookup because there could be lookup values. Called whenever columns definition changes, i.e. in setData() and clearColumns(). @see find() */ void updateIndicesForVisibleValues(); //! @return horizontal header, 0 by default. virtual QHeaderView* horizontalHeader() const; //! @return vertical header, 0 by default. virtual QHeaderView* verticalHeader() const; //! Update section of vertical header virtual void updateVerticalHeaderSection(int section) = 0; //! data structure displayed for this object KDbTableViewData *m_data; //! current record (cursor) int m_curRecord; //! current column (cursor) int m_curColumn; //! current record's data KDbRecordData *m_currentRecord; //! data iterator KDbTableViewDataIterator m_itemIterator; //! record's data for inserting KDbRecordData *m_insertRecord; //! true if m_data member is owned by this object bool m_owner; /*! true if a new record is edited; implies: m_recorfEditing == true. */ bool m_newRecordEditing; /*! 'sorting by column' availability flag for widget */ bool m_isSortingEnabled; /*! true if filtering is enabled for the view. */ bool m_isFilteringEnabled; /*! Public version of 'acceptsRecordEditingAfterCellAcceptin' flag (available for a user). It's OR'es together with above flag. */ bool m_acceptsRecordEditAfterCellAccepting; /*! Used in acceptEditor() to avoid infinite recursion, eg. when we're calling acceptRecordEditing() during cell accepting phase. */ bool m_inside_acceptEditor; // no bit field allowed /*! Used in acceptRecordEditing() to avoid infinite recursion, eg. when we're calling acceptRecordEdit() during cell accepting phase. */ bool m_inside_acceptRecordEdit; // no bit field allowed /*! @internal if true, this object automatically accepts record editing (using acceptRecordEditing()) on accepting any cell's edit (i.e. after acceptEditor()). */ bool m_internal_acceptsRecordEditingAfterCellAccepting; /*! true, if inserting empty records are enabled (false by default) */ bool m_emptyRecordInsertingEnabled; /*! Contains 1 if the object is readOnly, 0 if not; otherwise (-1 means "do not know") the 'readOnly' flag from object's internal data structure (KDbTableViewData *KexiTableView::m_data) is reused. */ int m_readOnly; //! @todo really keep this here and not in KexiTableView? /*! true if currently double click action was is performed (so accept/cancel editor shoudn't be executed) */ bool m_contentsMousePressEvent_dblClick; /*! like for readOnly: 1 if inserting is enabled */ int m_insertingEnabled; /*! true, if initDataContents() should be called on show event. */ bool m_initDataContentsOnShow; /*! Set to true in setCursorPosition() to indicate that cursor position was set before show() and it shouldn't be changed on show(). Only used if initDataContentsOnShow is true. */ bool m_cursorPositionSetExplicityBeforeShow; /*! true if spreadSheetMode is enabled. False by default. @see KexiTableView::setSpreadSheetMode() */ bool m_spreadSheetMode; /*! true, if this table accepts dropping data on the records (false by default). */ bool m_dropsAtRecordEnabled; /*! true, if this entire (visible) record should be updated when boving to other record. False by default. For table view with 'record highlighting' flag enabled, it is true. */ bool m_updateEntireRecordWhenMovingToOtherRecord; DeletionPolicy m_deletionPolicy; KexiDataItemInterface *m_editor; /*! Navigation panel, used if navigationPanelEnabled is true. */ KexiRecordNavigatorIface *m_navPanel; //!< main navigation widget bool m_navPanelEnabled; /*! Record number that over which user drags a mouse pointer. Used to indicate dropping possibility for that record. Equal -1 if no indication is needed. */ int m_dragIndicatorLine; /*! Context menu widget. */ QMenu *m_contextMenu; /*! Text of context menu title. */ QString m_contextMenuTitleText; /*! Icon of context menu title. */ QIcon m_contextMenuTitleIcon; /*! True if context menu is enabled. */ bool m_contextMenuEnabled; //! Used by updateAfterCancelRecordEditing() bool m_alsoUpdateNextRecord; /*! Record number (>=0 or -1 == no record) that will be deleted in deleteRecord(). It is set in slotAboutToDeleteRecord(KDbRecordData*,KDbResultInfo*,bool)) slot received from KDbTableViewData member. This value will be used in slotRecordDeleted() after recordDeleted() signal is received from KDbTableViewData member and then cleared (set to -1). */ int m_recordWillBeDeleted; /*! Displays passive error popup label used when invalid data has been entered. */ QPointer m_errorMessagePopup; /*! Used to enable/disable execution of verticalScrollBarValueChanged() when users navigate through records using keyboard, so scrollbar tooltips are not visible. */ bool m_verticalScrollBarValueChanged_enabled; /*! True, if vscrollbar tooltips are enabled (true by default). */ bool m_scrollbarToolTipsEnabled; //! Used to mark recently found value class PositionOfValue { public: PositionOfValue() : firstCharacter(0), lastCharacter(0), exists(false) {} int firstCharacter; int lastCharacter; bool exists; }; /*! Used to mark recently found value. Updated on successful execution of find(). If the current cursor's position changes, or data in the current cell changes, positionOfRecentlyFoundValue.exists is set to false. */ PositionOfValue m_positionOfRecentlyFoundValue; /*! Used to compare whether we're looking for new value. */ QVariant m_recentlySearchedValue; /*! Used to compare whether the search direction has changed. */ KexiSearchAndReplaceViewInterface::Options::SearchDirection m_recentSearchDirection; //! Setup by updateIndicesForVisibleValues() and used by find() QVector m_indicesForVisibleValues; private: /*! >= 0 if a record is edited */ int m_recordEditing; bool m_lengthExceededMessageVisible; //! true if acceptRecordEditing() should be called in setCursorPosition() (true by default) bool m_acceptRecordEditing_in_setCursorPosition_enabled; }; Q_DECLARE_OPERATORS_FOR_FLAGS(KexiDataAwareObjectInterface::CreateEditorFlags) Q_DECLARE_OPERATORS_FOR_FLAGS(KexiDataAwareObjectInterface::CursorPositionFlags) inline bool KexiDataAwareObjectInterface::hasData() const { if (!m_data) qDebug() << "KexiDataAwareObjectInterface: No data assigned!"; return m_data != 0; } inline KDbRecordData *KexiDataAwareObjectInterface::recordAt(int pos) const { KDbRecordData *data = m_data->at(pos); if (!data) qDebug() << "pos:" << pos << "- NO ITEM!!"; else { /* qDebug() << "record:" << record; int i=1; for (KexiTableItem::Iterator it = item->begin();it!=item->end();++it,i++) qDebug() << i<<": " << (*it).toString();*/ } return data; } //! Convenience macro used for KexiDataAwareObjectInterface implementations. #define KEXI_DATAAWAREOBJECTINTERFACE \ public: \ void connectCellSelectedSignal(const QObject* receiver, const char* intIntMember) { \ connect(this, SIGNAL(cellSelected(int,int)), receiver, intIntMember); \ } \ void connectRecordEditingStartedSignal(const QObject* receiver, const char* intMember) { \ connect(this, SIGNAL(recordEditingStarted(int)), receiver, intMember); \ } \ void connectRecordEditingTerminatedSignal(const QObject* receiver, const char* voidMember) { \ connect(this, SIGNAL(recordEditingTerminated(int)), receiver, voidMember); \ } \ void connectUpdateSaveCancelActionsSignal(const QObject* receiver, \ const char* voidMember) { \ connect(this, SIGNAL(updateSaveCancelActions()), receiver, voidMember); \ } \ void connectReloadActionsSignal(const QObject* receiver, const char* voidMember) { \ connect(this, SIGNAL(reloadActions()), receiver, voidMember); \ } \ void connectDataSetSignal(const QObject* receiver, \ const char* kexiTableViewDataMember) { \ connect(this, SIGNAL(dataSet(KDbTableViewData*)), receiver, kexiTableViewDataMember); \ } \ void connectToReloadDataSlot(const QObject* sender, const char* voidSignal) { \ connect(sender, voidSignal, this, SLOT(reloadData())); \ } #endif diff --git a/src/widget/tableview/KexiTableScrollArea.cpp b/src/widget/tableview/KexiTableScrollArea.cpp index 73b18f5bd..eb1a7a02f 100644 --- a/src/widget/tableview/KexiTableScrollArea.cpp +++ b/src/widget/tableview/KexiTableScrollArea.cpp @@ -1,2496 +1,2496 @@ /* This file is part of the KDE project Copyright (C) 2002 Till Busch Copyright (C) 2003 Lucijan Busch Copyright (C) 2003 Daniel Molkentin Copyright (C) 2003 Joseph Wenninger Copyright (C) 2003-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. Original Author: Till Busch Original Project: buX (www.bux.at) */ #include "KexiTableScrollArea.h" #include "KexiTableScrollArea_p.h" #include "KexiTableScrollAreaWidget.h" #include "KexiTableScrollAreaHeader.h" #include "KexiTableScrollAreaHeaderModel.h" #include "kexitableedit.h" #include #include #include "kexicelleditorfactory.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 #ifdef KEXI_TABLE_PRINT_SUPPORT #include #endif //#define KEXITABLEVIEW_DEBUG //#define KEXITABLEVIEW_DEBUG_PAINT const int MINIMUM_ROW_HEIGHT = 17; KexiTableScrollArea::Appearance::Appearance(QWidget *widget) { //set defaults if (qApp) { baseColor = KColorScheme(QPalette::Active, KColorScheme::View).background().color()/*QPalette::Base*/; textColor = KColorScheme(QPalette::Active, KColorScheme::View).foreground().color()/*QPalette::Base*/; QStyleOptionViewItemV4 option; option.initFrom(widget); const int gridHint = widget->style()->styleHint(QStyle::SH_Table_GridLineColor, &option, widget); gridColor = static_cast(gridHint); emptyAreaColor = KColorScheme(QPalette::Active, KColorScheme::View).background().color()/*QPalette::Base*/; alternateBaseColor = widget->palette().color(QPalette::AlternateBase); recordHighlightingColor = KexiUtils::blendedColors(QPalette::Highlight, baseColor, 34, 66); recordMouseOverHighlightingColor = KexiUtils::blendedColors(QPalette::Highlight, baseColor, 10, 90); recordMouseOverAlternateHighlightingColor = KexiUtils::blendedColors(QPalette::Highlight, alternateBaseColor, 10, 90); recordHighlightingTextColor = textColor; recordMouseOverHighlightingTextColor = textColor; } backgroundAltering = true; recordMouseOverHighlightingEnabled = true; recordHighlightingEnabled = true; persistentSelections = true; navigatorEnabled = true; fullRecordSelection = false; verticalGridEnabled = true; horizontalGridEnabled = !backgroundAltering || baseColor == alternateBaseColor; } //----------------------------------------- //! @todo KEXI3 KexiTableViewCellToolTip /* TODO KexiTableViewCellToolTip::KexiTableViewCellToolTip( KexiTableView * tableView ) : QToolTip() , m_tableView(tableView) { } KexiTableViewCellToolTip::~KexiTableViewCellToolTip() { } void KexiTableViewCellToolTip::maybeTip( const QPoint & p ) { const QPoint cp( m_tableView->viewportToContents( p ) ); const int row = m_tableView->recordAt( cp.y(), true ); const int col = m_tableView->columnAt( cp.x() ); //show tooltip if needed if (col>=0 && row>=0) { KexiTableEdit *editor = m_tableView->tableEditorWidget( col ); const bool insertRowSelected = m_tableView->isInsertingEnabled() && row==m_tableView->rowCount(); KDbRecordData *data = insertRowSelected ? m_tableView->m_insertItem : m_tableView->itemAt( row ); if (editor && record && (col < (int)record->count())) { int w = m_tableView->columnWidth( col ); int h = m_tableView->rowHeight(); int x = 0; int y_offset = 0; int align = SingleLine | AlignVCenter; QString txtValue; QVariant cellValue; KDbTableViewColumn *tvcol = m_tableView->column(col); if (!m_tableView->getVisibleLookupValue(cellValue, editor, record, tvcol)) cellValue = insertRowSelected ? editor->displayedField()->defaultValue() : record->at(col); //display default value if available const bool focused = m_tableView->selectedRecord() == record && col == m_tableView->currentColumn(); editor->setupContents( 0, focused, cellValue, txtValue, align, x, y_offset, w, h ); QRect realRect(m_tableView->columnPos(col)-m_tableView->horizontalScrollBar()->value(), m_tableView->recordPos(row)-m_tableView->verticalScrollBar()->value(), w, h); if (editor->showToolTipIfNeeded( txtValue.isEmpty() ? record->at(col) : QVariant(txtValue), realRect, m_tableView->fontMetrics(), focused)) { QString squeezedTxtValue; if (txtValue.length() > 50) squeezedTxtValue = txtValue.left(100) + "..."; else squeezedTxtValue = txtValue; tip( realRect, squeezedTxtValue ); } } } } */ //TODO //----------------------------------------- KexiTableScrollArea::KexiTableScrollArea(KDbTableViewData* data, QWidget* parent) : QScrollArea(parent) , KexiRecordNavigatorHandler() , KexiSharedActionClient() , KexiDataAwareObjectInterface() , d(new Private(this)) { setAttribute(Qt::WA_StaticContents, true); setAttribute(Qt::WA_CustomWhatsThis, true); d->scrollAreaWidget = new KexiTableScrollAreaWidget(this); setWidget(d->scrollAreaWidget); m_data = new KDbTableViewData(); //to prevent crash because m_data==0 m_owner = true; //-this will be deleted if needed viewport()->setFocusPolicy(Qt::WheelFocus); setFocusPolicy(Qt::WheelFocus); //<--- !!!!! important (was NoFocus), // otherwise QApplication::setActiveWindow() won't activate // this widget when needed! viewport()->installEventFilter(this); d->scrollAreaWidget->installEventFilter(this); d->diagonalGrayPattern = QBrush(d->appearance.gridColor, Qt::BDiagPattern); setLineWidth(1); horizontalScrollBar()->installEventFilter(this); //context menu m_contextMenu = new QMenu(this); m_contextMenu->setObjectName("m_contextMenu"); //! \todo replace lineedit with table_field icon //setContextMenuTitle(KexiIcon("lineedit"), xi18n("Record")); // the default // cannot display anything here - most actions in the context menu // are related to a single cell (Cut, Copy..) and others to entire row (Delete Row): setContextMenuEnabled(false); d->pUpdateTimer = new QTimer(this); d->pUpdateTimer->setSingleShot(true); // Create headers d->headerModel = new KexiTableScrollAreaHeaderModel(this); d->horizontalHeader = new KexiTableScrollAreaHeader(Qt::Horizontal, this); d->horizontalHeader->setObjectName("horizontalHeader"); d->horizontalHeader->setSelectionBackgroundColor(palette().color(QPalette::Highlight)); d->verticalHeader = new KexiTableScrollAreaHeader(Qt::Vertical, this); d->verticalHeader->setObjectName("verticalHeader"); d->verticalHeader->setSelectionBackgroundColor(palette().color(QPalette::Highlight)); setupNavigator(); if (data) { setData(data); } setAcceptDrops(true); viewport()->setAcceptDrops(true); // Connect header, table and scrollbars connect(horizontalScrollBar(), SIGNAL(valueChanged(int)), d->horizontalHeader, SLOT(setOffset(int))); connect(verticalScrollBar(), SIGNAL(valueChanged(int)), d->verticalHeader, SLOT(setOffset(int))); connect(d->horizontalHeader, SIGNAL(sectionResized(int,int,int)), this, SLOT(slotColumnWidthChanged(int,int,int))); connect(d->horizontalHeader, SIGNAL(sectionHandleDoubleClicked(int)), this, SLOT(slotSectionHandleDoubleClicked(int))); connect(d->horizontalHeader, SIGNAL(sectionClicked(int)), this, SLOT(sortColumnInternal(int))); connect(d->pUpdateTimer, SIGNAL(timeout()), this, SLOT(slotUpdate())); setAppearance(d->appearance); //refresh d->setSpreadSheetMode(false); //! @todo KEXI3 d->cellToolTip = new KexiTableViewCellToolTip(this); } KexiTableScrollArea::~KexiTableScrollArea() { cancelRecordEditing(); KDbTableViewData *data = m_data; m_data = 0; if (m_owner) { if (data) data->deleteLater(); } delete d; } void KexiTableScrollArea::clearVariables() { KexiDataAwareObjectInterface::clearVariables(); d->clearVariables(); } void KexiTableScrollArea::setupNavigator() { m_navPanel = new KexiRecordNavigator(*this, this); navPanelWidget()->setObjectName("navPanel"); m_navPanel->setRecordHandler(this); } void KexiTableScrollArea::initDataContents() { updateWidgetContentsSize(); KexiDataAwareObjectInterface::initDataContents(); m_navPanel->showEditingIndicator(false); } void KexiTableScrollArea::updateWidgetContentsSize() { updateScrollAreaWidgetSize(); d->horizontalHeader->setFixedSize(d->horizontalHeader->sizeHint()); d->verticalHeader->setFixedSize(d->verticalHeader->sizeHint()); //qDebug() << d->horizontalHeader->sizeHint() << d->verticalHeader->sizeHint(); //qDebug() << d->horizontalHeader->geometry() << d->verticalHeader->geometry(); } void KexiTableScrollArea::updateScrollAreaWidgetSize() { QSize s(tableSize()); const int colOffset = d->columnOffset(); s.setWidth(qMax(s.width() + colOffset, viewport()->width())); s.setHeight(qMax(s.height() + colOffset, viewport()->height())); d->scrollAreaWidget->resize(s); } void KexiTableScrollArea::updateVerticalHeaderSection(int row) { d->verticalHeader->updateSection(row); } void KexiTableScrollArea::slotRecordsDeleted(const QList &records) { viewport()->repaint(); updateWidgetContentsSize(); setCursorPosition(qMax(0, (int)m_curRecord - (int)records.count()), -1, ForceSetCursorPosition); } void KexiTableScrollArea::setFont(const QFont &font) { QScrollArea::setFont(font); #ifdef Q_OS_WIN //! @todo KEXI3 WIN32 test this d->recordHeight = fontMetrics().lineSpacing() + 4; #else d->recordHeight = fontMetrics().lineSpacing() + 1; #endif if (d->appearance.fullRecordSelection) { d->recordHeight -= 1; } if (d->recordHeight < MINIMUM_ROW_HEIGHT) { d->recordHeight = MINIMUM_ROW_HEIGHT; } KexiDisplayUtils::initDisplayForAutonumberSign(&d->autonumberSignDisplayParameters, this); KexiDisplayUtils::initDisplayForDefaultValue(&d->defaultValueDisplayParameters, this); update(); } void KexiTableScrollArea::updateAllVisibleRecordsBelow(int record) { //get last visible row // int r = recordAt(viewport()->height() + verticalScrollBar()->value()); // if (r == -1) { // r = rowCount() + 1 + (isInsertingEnabled() ? 1 : 0); // } //update all visible rows below int leftcol = d->horizontalHeader->visualIndexAt(d->horizontalHeader->offset()); d->scrollAreaWidget->update(columnPos(leftcol), recordPos(record), viewport()->width(), viewport()->height() - (recordPos(record) - verticalScrollBar()->value())); } void KexiTableScrollArea::clearColumnsInternal(bool /*repaint*/) { } void KexiTableScrollArea::slotUpdate() { // qDebug() << m_navPanel; updateScrollAreaWidgetSize(); d->scrollAreaWidget->update(); updateWidgetContentsSize(); } -Qt::SortOrder KexiTableScrollArea::currentLocalSortOrder() const +KDbOrderByColumn::SortOrder KexiTableScrollArea::currentLocalSortOrder() const { - return d->horizontalHeader->sortIndicatorOrder(); + return KDbOrderByColumn::fromQt(d->horizontalHeader->sortIndicatorOrder()); } -void KexiTableScrollArea::setLocalSortOrder(int column, Qt::SortOrder order) +void KexiTableScrollArea::setLocalSortOrder(int column, KDbOrderByColumn::SortOrder order) { - d->horizontalHeader->setSortIndicator(column, order); + d->horizontalHeader->setSortIndicator(column, KDbOrderByColumn::toQt(order)); } int KexiTableScrollArea::currentLocalSortColumn() const { return d->horizontalHeader->sortIndicatorSection(); } void KexiTableScrollArea::updateGUIAfterSorting(int previousRow) { int prevRowVisibleOffset = recordPos(previousRow) - verticalScrollBar()->value(); verticalScrollBar()->setValue(recordPos(m_curRecord) - prevRowVisibleOffset); d->scrollAreaWidget->update(); selectCellInternal(m_curRecord, m_curColumn); } QSizePolicy KexiTableScrollArea::sizePolicy() const { // this widget is expandable return QSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); } QSize KexiTableScrollArea::sizeHint() const { QSize ts = tableSize(); int w = qMax(ts.width() + leftMargin() + verticalScrollBar()->sizeHint().width() + 2 * 2, (navPanelWidgetVisible() ? navPanelWidget()->width() : 0)); int h = qMax(ts.height() + topMargin() + horizontalScrollBar()->sizeHint().height(), minimumSizeHint().height()); w = qMin(w, qApp->desktop()->availableGeometry(this).width() * 3 / 4); //stretch h = qMin(h, qApp->desktop()->availableGeometry(this).height() * 3 / 4); //stretch #ifdef KEXITABLEVIEW_DEBUG qDebug() << w << h; #endif return QSize(w, h); } QSize KexiTableScrollArea::minimumSizeHint() const { return QSize( leftMargin() + ((columnCount() > 0) ? columnWidth(0) : KEXI_DEFAULT_DATA_COLUMN_WIDTH) + 2*2, d->recordHeight*5 / 2 + topMargin() + (navPanelWidgetVisible() ? navPanelWidget()->height() : 0) ); } QRect KexiTableScrollArea::viewportGeometry() const { return viewport()->geometry(); } //internal inline void KexiTableScrollArea::paintRow(KDbRecordData *data, QPainter *pb, int r, int rowp, int cx, int cy, int colfirst, int collast, int maxwc) { Q_UNUSED(cx); Q_UNUSED(cy); if (!data) return; //qDebug() << "r" << r << "rowp" << rowp << "cx" << cx << "cy" << cy // << "colfirst" << colfirst << "collast" << collast << "maxwc" << maxwc; // Go through the columns in the row r // if we know from where to where, go through [colfirst, collast], // else go through all of them if (colfirst == -1) colfirst = 0; if (collast == -1) collast = columnCount() - 1; int transly = rowp; if (d->appearance.recordHighlightingEnabled && r == m_curRecord && !d->appearance.fullRecordSelection) { pb->fillRect(0, transly, maxwc, d->recordHeight, d->appearance.recordHighlightingColor); } else if (d->appearance.recordMouseOverHighlightingEnabled && r == d->highlightedRecord) { if (d->appearance.backgroundAltering && (r % 2 != 0)) pb->fillRect(0, transly, maxwc, d->recordHeight, d->appearance.recordMouseOverAlternateHighlightingColor); else pb->fillRect(0, transly, maxwc, d->recordHeight, d->appearance.recordMouseOverHighlightingColor); } else { if (d->appearance.backgroundAltering && (r % 2 != 0)) pb->fillRect(0, transly, maxwc, d->recordHeight, d->appearance.alternateBaseColor); else pb->fillRect(0, transly, maxwc, d->recordHeight, d->appearance.baseColor); } for (int c = colfirst; c <= collast; c++) { // get position and width of column c int colp = columnPos(c); if (colp == -1) continue; //invisible column? int colw = columnWidth(c); // qDebug() << "c:" << c << "colp:" << colp << "cx:" << cx << "contentsX():" << horizontalScrollBar()->value() << "colw:" << colw; //(js #2010-01-05) breaks rendering: int translx = colp - cx + horizontalScrollBar()->value(); int translx = colp; // Translate painter and draw the cell const QTransform oldTr( pb->worldTransform() ); pb->translate(translx, transly); paintCell(pb, data, r, c, QRect(colp, rowp, colw, d->recordHeight)); pb->setWorldTransform(oldTr); } if (m_dragIndicatorLine >= 0) { int y_line = -1; if (r == (recordCount() - 1) && m_dragIndicatorLine == recordCount()) { y_line = transly + d->recordHeight - 3; //draw at last line } if (m_dragIndicatorLine == r) { y_line = transly; } if (y_line >= 0) { if (!d->dragIndicatorRubberBand) { d->dragIndicatorRubberBand = new QRubberBand(QRubberBand::Line, viewport()); } d->dragIndicatorRubberBand->setGeometry(0, y_line, maxwc, 3); d->dragIndicatorRubberBand->show(); } else { if (d->dragIndicatorRubberBand) { d->dragIndicatorRubberBand->hide(); } } } else { if (d->dragIndicatorRubberBand) { d->dragIndicatorRubberBand->hide(); } } } void KexiTableScrollArea::drawContents(QPainter *p) { int cx = p->clipBoundingRect().x(); int cy = p->clipBoundingRect().y(); int cw = p->clipBoundingRect().width(); int ch = p->clipBoundingRect().height(); #ifdef KEXITABLEVIEW_DEBUG qDebug() << "disable" << d->disableDrawContents << "cx" << cx << "cy" << cy << "cw" << cw << "ch" << ch << "contentsRect" << contentsRect() << "geo" << geometry(); #endif if (d->disableDrawContents) return; bool paintOnlyInsertRow = false; bool inserting = isInsertingEnabled(); bool plus1row = false; //true if we should show 'inserting' row at the end int colfirst = columnNumberAt(cx); int rowfirst = recordNumberAt(cy); int collast = columnNumberAt(cx + cw - 1); int rowlast = recordNumberAt(cy + ch - 1); if (rowfirst == -1 && (cy / d->recordHeight) == recordCount()) { // make the insert row paint too when requested #ifdef KEXITABLEVIEW_DEBUG qDebug() << "rowfirst == -1 && (cy / d->rowHeight) == rowCount()"; #endif rowfirst = m_data->count(); rowlast = rowfirst; paintOnlyInsertRow = true; plus1row = inserting; } /* qDebug() << "cx" << cx << "cy" << cy << "cw" << cw << "ch" << ch << "colfirst" << colfirst << "rowfirst" << rowfirst << "collast" << collast << "rowlast" << rowlast;*/ if (rowlast == -1) { rowlast = recordCount() - 1; plus1row = inserting; if (rowfirst == -1) { if (recordNumberAt(cy - d->recordHeight) != -1) { //paintOnlyInsertRow = true; // qDebug() << "-- paintOnlyInsertRow --"; } } } // qDebug() << "rowfirst="<fillRect(cx, cy, cw, ch, d->appearance.baseColor); int rowp = 0; int r = 0; if (paintOnlyInsertRow) { r = recordCount(); rowp = recordPos(r); // 'insert' row's position } else { if (rowfirst >= 0) { QList::ConstIterator it(m_data->constBegin()); it += rowfirst;//move to 1st row rowp = recordPos(rowfirst); // row position for (r = rowfirst;r <= rowlast; r++, ++it, rowp += d->recordHeight) { // qDebug() << *it; paintRow(*it, p, r, rowp, cx, cy, colfirst, collast, maxwc); } } } if (plus1row && rowfirst >= 0) { //additional - 'insert' row paintRow(m_insertRecord, p, r, rowp, cx, cy, colfirst, collast, maxwc); } paintEmptyArea(p, cx, cy, cw, ch); } bool KexiTableScrollArea::isDefaultValueDisplayed(KDbRecordData *data, int col, QVariant* value) { const bool cursorAtInsertRowOrEditingNewRow = (data == m_insertRecord || (m_newRecordEditing && m_currentRecord == data)); KDbTableViewColumn *tvcol; if (cursorAtInsertRowOrEditingNewRow && (tvcol = m_data->column(col)) && hasDefaultValueAt(*tvcol) && !tvcol->field()->isAutoIncrement()) { if (value) *value = tvcol->field()->defaultValue(); return true; } return false; } void KexiTableScrollArea::paintCell(QPainter* p, KDbRecordData *data, int record, int column, const QRect &cr, bool print) { Q_UNUSED(print); //qDebug() << "col/row:" << col << row << "rect:" << cr; p->save(); int w = cr.width(); int h = cr.height(); int x2 = w - 1; int y2 = h - 1; // Draw our lines QPen pen(p->pen()); if (d->appearance.horizontalGridEnabled) { p->setPen(d->appearance.gridColor); p->drawLine(0, y2, x2, y2); // bottom } if (d->appearance.verticalGridEnabled) { p->setPen(d->appearance.gridColor); p->drawLine(x2, 0, x2, y2); // right } p->setPen(pen); if (m_editor && record == m_curRecord && column == m_curColumn //don't paint contents of edited cell && m_editor->hasFocusableWidget() //..if it's visible ) { p->restore(); return; } KexiTableEdit *edit = tableEditorWidget(column, /*ignoreMissingEditor=*/true); int x = edit ? edit->leftMargin() : 0; int y_offset = 0; int align = Qt::TextSingleLine | Qt::AlignVCenter; QString txt; //text to draw if (data == m_insertRecord) { //qDebug() << "we're at INSERT row..."; } KDbTableViewColumn *tvcol = m_data->column(column); QVariant cellValue; if (column < (int)data->count()) { if (m_currentRecord == data) { if (m_editor && record == m_curRecord && column == m_curColumn && !m_editor->hasFocusableWidget()) { //we're over editing cell and the editor has no widget // - we're displaying internal values, not buffered cellValue = m_editor->value(); } else { //we're displaying values from edit buffer, if available // this assignment will also get default value if there's no actual value set cellValue = *bufferedValueAt(record, column); } } else { cellValue = data->at(column); } } bool defaultValueDisplayed = isDefaultValueDisplayed(data, column); if (data == m_insertRecord && cellValue.isNull()) { if (!tvcol->field()->isAutoIncrement() && !tvcol->field()->defaultValue().isNull()) { //display default value in the "insert record", if available //(but not if there is autoincrement flag set) cellValue = tvcol->field()->defaultValue(); defaultValueDisplayed = true; } } const bool columnReadOnly = tvcol->isReadOnly(); const bool dontPaintNonpersistentSelectionBecauseDifferentRowHasBeenHighlighted = d->appearance.recordHighlightingEnabled && !d->appearance.persistentSelections && m_curRecord >= 0 && record != m_curRecord; // setup default pen QPen defaultPen; const bool usesSelectedTextColor = edit && edit->usesSelectedTextColor(); if (defaultValueDisplayed){ if (column == m_curColumn && record == m_curRecord && usesSelectedTextColor) defaultPen = d->defaultValueDisplayParameters.selectedTextColor; else defaultPen = d->defaultValueDisplayParameters.textColor; } else if (d->appearance.fullRecordSelection && (record == d->highlightedRecord || (record == m_curRecord && d->highlightedRecord == -1)) && usesSelectedTextColor) { defaultPen = d->appearance.recordHighlightingTextColor; //special case: highlighted record } else if (d->appearance.fullRecordSelection && record == m_curRecord && usesSelectedTextColor) { defaultPen = d->appearance.textColor; //special case for full record selection } else if ( m_currentRecord == data && column == m_curColumn && !columnReadOnly && !dontPaintNonpersistentSelectionBecauseDifferentRowHasBeenHighlighted && usesSelectedTextColor) { defaultPen = palette().color(QPalette::HighlightedText); //selected text } else if ( d->appearance.recordHighlightingEnabled && record == m_curRecord && !dontPaintNonpersistentSelectionBecauseDifferentRowHasBeenHighlighted && usesSelectedTextColor) { defaultPen = d->appearance.recordHighlightingTextColor; } else if ( d->appearance.recordMouseOverHighlightingEnabled && record == d->highlightedRecord && !dontPaintNonpersistentSelectionBecauseDifferentRowHasBeenHighlighted && usesSelectedTextColor) { defaultPen = d->appearance.recordMouseOverHighlightingTextColor; } else { defaultPen = d->appearance.textColor; } if (edit) { if (defaultValueDisplayed) p->setFont(d->defaultValueDisplayParameters.font); p->setPen(defaultPen); //get visible lookup value if available getVisibleLookupValue(cellValue, edit, data, tvcol); /*qDebug() << "edit->setupContents()" << (m_currentRecord == record && col == m_curColumn) << cellValue << txt << align << x << y_offset << w << h;*/ edit->setupContents(p, m_currentRecord == data && column == m_curColumn, cellValue, txt, align, x, y_offset, w, h); } if (!d->appearance.horizontalGridEnabled) y_offset++; //correction because we're not drawing cell borders if (d->appearance.fullRecordSelection) { } if (m_currentRecord == data && (column == m_curColumn || d->appearance.fullRecordSelection)) { if (edit && ( (d->appearance.recordHighlightingEnabled && !d->appearance.fullRecordSelection) || (record == m_curRecord && d->highlightedRecord == -1 && d->appearance.fullRecordSelection)) ) { edit->paintSelectionBackground(p, isEnabled(), txt, align, x, y_offset, w, h, isEnabled() ? palette().color(QPalette::Highlight) : QColor(200, 200, 200),//d->grayColor, p->fontMetrics(), columnReadOnly, d->appearance.fullRecordSelection); } } if (!edit) { p->fillRect(0, 0, x2, y2, d->diagonalGrayPattern); } // If we are in the focus cell, draw indication if ( m_currentRecord == data && column == m_curColumn //js: && !d->recordIndicator) && !d->appearance.fullRecordSelection) { // qDebug() << ">>> CURRENT CELL ("<field()->isAutoIncrement()) { // "autonumber" column KexiDisplayUtils::paintAutonumberSign(d->autonumberSignDisplayParameters, p, x, y_offset, w - x - x - ((align & Qt::AlignLeft) ? 2 : 0), h, (Qt::Alignment)align); } } // draw text if (!txt.isEmpty()) { if (defaultValueDisplayed) p->setFont(d->defaultValueDisplayParameters.font); p->setPen(defaultPen); p->drawText(x, y_offset, w - (x + x) - ((align & Qt::AlignLeft) ? 2 : 0)/*right space*/, h, align, txt); } #ifdef KEXITABLEVIEW_DEBUG_PAINT p->setPen(QPen(QColor(255, 0, 0, 150), 1, Qt::DashLine)); p->drawRect(x, y_offset, w - 1, h - 1); qDebug() << cellValue << "x:" << x << "y:" << y_offset << "w:" << w << "h:" << h; #endif p->restore(); } QPoint KexiTableScrollArea::contentsToViewport2(const QPoint &p) { return QPoint(p.x() - horizontalScrollBar()->value(), p.y() - verticalScrollBar()->value()); } void KexiTableScrollArea::contentsToViewport2(int x, int y, int& vx, int& vy) { const QPoint v = contentsToViewport2(QPoint(x, y)); vx = v.x(); vy = v.y(); } QPoint KexiTableScrollArea::viewportToContents2(const QPoint& vp) { return QPoint(vp.x() + horizontalScrollBar()->value(), vp.y() + verticalScrollBar()->value()); } void KexiTableScrollArea::paintEmptyArea(QPainter *p, int cx, int cy, int cw, int ch) { //qDebug() << cx << cy << cw << ch; // Regions work with shorts, so avoid an overflow and adjust the // table size to the visible size QSize ts(tableSize()); //qDebug() << ts; /* qDebug() << QString(" (cx:%1 cy:%2 cw:%3 ch:%4)") .arg(cx).arg(cy).arg(cw).arg(ch); qDebug() << QString(" (w:%3 h:%4)") .arg(ts.width()).arg(ts.height());*/ // Region of the rect we should draw, calculated in viewport // coordinates, as a region can't handle bigger coordinates contentsToViewport2(cx, cy, cx, cy); QRegion reg(QRect(cx, cy, cw, ch)); //qDebug() << "---cy-- " << verticalScrollBar()->value(); // Subtract the table from it reg = reg.subtracted(QRect(QPoint(0, 0), ts - QSize(0, + verticalScrollBar()->value()))); // And draw the rectangles (transformed inc contents coordinates as needed) const QVector rects(reg.rects()); foreach(const QRect& rect, rects) { QRect realRect(viewportToContents2(rect.topLeft()), rect.size()); // qDebug() << QString("- pEA: p->fillRect(x:%1 y:%2 w:%3 h:%4)") // .arg(rect.x()).arg(rect.y()) // .arg(rect.width()).arg(rect.height()) // << viewportGeometry(); p->fillRect(realRect, d->appearance.emptyAreaColor); } } void KexiTableScrollArea::contentsMouseDoubleClickEvent(QMouseEvent *e) { // qDebug(); m_contentsMousePressEvent_dblClick = true; contentsMousePressEvent(e); m_contentsMousePressEvent_dblClick = false; if (m_currentRecord) { if (d->editOnDoubleClick && columnEditable(m_curColumn) && columnType(m_curColumn) != KDbField::Boolean) { KexiTableEdit *edit = tableEditorWidget(m_curColumn, /*ignoreMissingEditor=*/true); if (edit && edit->handleDoubleClick()) { //nothing to do: editors like BLOB editor has custom handling of double clicking } else { startEditCurrentCell(); // createEditor(m_curRecord, m_curColumn, QString()); } } emit itemDblClicked(m_currentRecord, m_curRecord, m_curColumn); } } void KexiTableScrollArea::contentsMousePressEvent(QMouseEvent* e) { setFocus(); if (m_data->isEmpty() && !isInsertingEnabled()) { return; } //qDebug() << e->pos(); const int x = e->pos().x(); if (columnNumberAt(x) == -1) { //outside a column return; } if (!d->moveCursorOnMouseRelease) { if (!handleContentsMousePressOrRelease(e, false)) return; } // qDebug()<< "by now the current items should be set, if not -> error + crash"; if (e->button() == Qt::RightButton) { showContextMenu(e->globalPos()); } else if (e->button() == Qt::LeftButton) { if (columnType(m_curColumn) == KDbField::Boolean && columnEditable(m_curColumn)) { //only accept clicking on the [x] rect (copied from KexiBoolTableEdit::setupContents()) int s = qMax(d->recordHeight - 5, 12); s = qMin(d->recordHeight - 3, s); s = qMin(columnWidth(m_curColumn) - 3, s); //avoid too large box const QRect r( columnPos(m_curColumn) + qMax(columnWidth(m_curColumn) / 2 - s / 2, 0), recordPos(m_curRecord) + d->recordHeight / 2 - s / 2 /*- 1*/, s, s); //qDebug() << r; if (r.contains(e->pos())) { // qDebug() << "e->x:" << e->x() << " e->y:" << e->y() << " " << recordPos(m_curRecord) << // " " << columnPos(m_curColumn); boolToggled(); } } //! @todo #if 0 else if (columnType(m_curColumn) == QVariant::StringList && columnEditable(m_curColumn)) { createEditor(m_curRecord, m_curColumn); } #endif } } void KexiTableScrollArea::contentsMouseReleaseEvent(QMouseEvent* e) { if (m_data->count() == 0 && !isInsertingEnabled()) return; if (d->moveCursorOnMouseRelease) handleContentsMousePressOrRelease(e, true); int col = columnNumberAt(e->pos().x()); int row = recordNumberAt(e->pos().y()); if (!m_currentRecord || col == -1 || row == -1 || col != m_curColumn || row != m_curRecord)//outside a current cell return; emit itemMouseReleased(m_currentRecord, m_curRecord, m_curColumn); } bool KexiTableScrollArea::handleContentsMousePressOrRelease(QMouseEvent* e, bool release) { Q_UNUSED(release); //qDebug() << "oldRow=" << m_curRecord << " oldCol=" << m_curColumn; int newrow, newcol; //compute clicked row nr const int x = e->pos().x(); if (isInsertingEnabled()) { if (recordNumberAt(e->pos().y()) == -1) { newrow = recordNumberAt(e->pos().y() - d->recordHeight); if (newrow == -1 && m_data->count() > 0) { return false; } newrow++; qDebug() << "Clicked just on 'insert' record."; } else { // get new focus cell newrow = recordNumberAt(e->pos().y()); } } else { if (recordNumberAt(e->pos().y()) == -1 || columnNumberAt(x) == -1) { return false; //clicked outside a grid } // get new focus cell newrow = recordNumberAt(e->pos().y()); } newcol = columnNumberAt(x); if (e->button() != Qt::NoButton) { setCursorPosition(newrow, newcol); } return true; } void KexiTableScrollArea::showContextMenu(const QPoint& _pos) { if (!d->contextMenuEnabled || m_contextMenu->isEmpty()) return; QPoint pos(_pos); if (pos == QPoint(-1, -1)) { pos = viewport()->mapToGlobal(QPoint(columnPos(m_curColumn), recordPos(m_curRecord) + d->recordHeight)); } //show own context menu if configured selectRecord(m_curRecord); m_contextMenu->exec(pos); } void KexiTableScrollArea::contentsMouseMoveEvent(QMouseEvent *e) { int row; const int col = columnNumberAt(e->x()); if (col < 0) { row = -1; } else { row = recordNumberAt(e->y(), true /*ignoreEnd*/); if (row > (recordCount() - 1 + (isInsertingEnabled() ? 1 : 0))) row = -1; //no row to paint } // qDebug() << " row="<modifiers() == Qt::ShiftModifier; if (action_name == "edit_delete_row") return k == Qt::Key_Delete && e->modifiers() == Qt::ControlModifier; if (action_name == "edit_delete") return k == Qt::Key_Delete && e->modifiers() == Qt::NoModifier; if (action_name == "edit_edititem") return k == Qt::Key_F2 && e->modifiers() == Qt::NoModifier; if (action_name == "edit_insert_empty_row") return k == Qt::Key_Insert && e->modifiers() == (Qt::ShiftModifier | Qt::ControlModifier); return false; } void KexiTableScrollArea::contentsContextMenuEvent(QContextMenuEvent* e) { const bool nobtn = e->modifiers() == Qt::NoModifier; if (nobtn && e->reason() == QContextMenuEvent::Keyboard) { showContextMenu(); } } void KexiTableScrollArea::keyPressEvent(QKeyEvent* e) { #ifdef KEXITABLEVIEW_DEBUG qDebug() << e; #endif if (!hasData()) return; // qDebug() << "key=" <key() << " txt=" <text(); const int k = e->key(); const bool ro = isReadOnly(); QWidget *w = focusWidget(); if (!w || (w != viewport() && w != this && (!m_editor || !KDbUtils::hasParent(dynamic_cast(m_editor), w)))) { //don't process stranger's events e->ignore(); return; } if (d->skipKeyPress) { d->skipKeyPress = false; e->ignore(); return; } if (m_currentRecord == 0 && (m_data->count() > 0 || isInsertingEnabled())) { setCursorPosition(0, 0); } else if (m_data->count() == 0 && !isInsertingEnabled()) { e->accept(); return; } if (m_editor) {// if a cell is edited, do some special stuff if (k == Qt::Key_Escape) { cancelEditor(); emit updateSaveCancelActions(); e->accept(); return; } else if (k == Qt::Key_Return || k == Qt::Key_Enter) { if (columnType(m_curColumn) == KDbField::Boolean) { boolToggled(); } else { acceptEditor(); } e->accept(); return; } } else if (recordEditing() >= 0) {// if a row is in edit mode, do some special stuff if (shortCutPressed(e, "data_save_row")) { qDebug() << "shortCutPressed!!!"; acceptRecordEditing(); return; } } if (k == Qt::Key_Return || k == Qt::Key_Enter) { emit itemReturnPressed(m_currentRecord, m_curRecord, m_curColumn); } int curRow = m_curRecord; int curCol = m_curColumn; const bool nobtn = e->modifiers() == Qt::NoModifier; bool printable = false; //check shared shortcuts if (!ro) { if (shortCutPressed(e, "edit_delete_row")) { deleteCurrentRecord(); e->accept(); return; } else if (shortCutPressed(e, "edit_delete")) { deleteAndStartEditCurrentCell(); e->accept(); return; } else if (shortCutPressed(e, "edit_insert_empty_row")) { insertEmptyRecord(); e->accept(); return; } } if (k == Qt::Key_Shift || k == Qt::Key_Alt || k == Qt::Key_Control || k == Qt::Key_Meta) { e->ignore(); } else if (KexiDataAwareObjectInterface::handleKeyPress( e, &curRow, &curCol, d->appearance.fullRecordSelection)) { if (e->isAccepted()) return; } else if (k == Qt::Key_Backspace && nobtn) { if (!ro && columnType(curCol) != KDbField::Boolean && columnEditable(curCol)) { const CreateEditorFlags flags = DefaultCreateEditorFlags | ReplaceOldValue; createEditor(curRow, curCol, QString(), flags); } } else if (k == Qt::Key_Space) { if (nobtn && !ro && columnEditable(curCol)) { if (columnType(curCol) == KDbField::Boolean) { boolToggled(); } else printable = true; //just space key } } else if (k == Qt::Key_Escape) { if (nobtn && recordEditing() >= 0) { cancelRecordEditing(); return; } } else { //others: if ((nobtn && k == Qt::Key_Tab) || k == Qt::Key_Right) { //! \todo add option for stopping at 1st column for Qt::Key_left //tab if (acceptEditor()) { if (curCol == (columnCount() - 1)) { if (curRow < (recordCount() - 1 + (isInsertingEnabled() ? 1 : 0))) {//skip to next row curRow++; curCol = 0; } } else curCol++; } } else if ((e->modifiers() == Qt::ShiftModifier && k == Qt::Key_Tab) || (nobtn && k == Qt::Key_Backtab) || (e->modifiers() == Qt::ShiftModifier && k == Qt::Key_Backtab) || k == Qt::Key_Left ) { //! \todo add option for stopping at last column //backward tab if (acceptEditor()) { if (curCol == 0) { if (curRow > 0) {//skip to previous row curRow--; curCol = columnCount() - 1; } } else curCol--; } } else { KexiTableEdit *edit = tableEditorWidget(m_curColumn); if (edit && edit->handleKeyPress(e, m_editor == edit)) { //try to handle the event @ editor's level e->accept(); return; } else if (nobtn && (k == Qt::Key_Enter || k == Qt::Key_Return || shortCutPressed(e, "edit_edititem"))) { //this condition is moved after handleKeyPress() to allow to everride enter key as well startEditOrToggleValue(); } else { qDebug() << "default"; if (e->text().isEmpty() || !e->text()[0].isPrint()) { qDebug() << "NOT PRINTABLE: 0x0" << QString("%1").arg(k, 0, 16); // e->ignore(); QScrollArea::keyPressEvent(e); return; } printable = true; } } } //finally: we've printable char: if (printable && !ro) { KDbTableViewColumn *tvcol = m_data->column(curCol); if (tvcol->acceptsFirstChar(e->text()[0])) { qDebug() << "ev pressed: acceptsFirstChar()==true"; const CreateEditorFlags flags = DefaultCreateEditorFlags | ReplaceOldValue; createEditor(curRow, curCol, e->text(), flags); } else { //! @todo show message "key not allowed eg. on a statusbar" qDebug() << "ev pressed: acceptsFirstChar()==false"; } } m_verticalScrollBarValueChanged_enabled = false; // if focus cell changes, repaint setCursorPosition(curRow, curCol, DontEnsureCursorVisibleIfPositionUnchanged); m_verticalScrollBarValueChanged_enabled = true; e->accept(); } void KexiTableScrollArea::emitSelected() { if (m_currentRecord) emit itemSelected(m_currentRecord); } int KexiTableScrollArea::recordsPerPage() const { return viewport()->height() / d->recordHeight; } KexiDataItemInterface *KexiTableScrollArea::editor(int col, bool ignoreMissingEditor) { if (!m_data || col < 0 || col >= columnCount()) return 0; KDbTableViewColumn *tvcol = m_data->column(col); //find the editor for this column KexiTableEdit *editor = d->editors.value(tvcol); if (editor) return editor; //not found: create editor = KexiCellEditorFactory::createEditor(*tvcol, d->scrollAreaWidget); if (!editor) {//create error! if (!ignoreMissingEditor) { //! @todo show error??? cancelRecordEditing(); } return 0; } editor->hide(); if (m_data->cursor() && m_data->cursor()->query()) editor->createInternalEditor(*m_data->cursor()->query()); connect(editor, SIGNAL(editRequested()), this, SLOT(slotEditRequested())); connect(editor, SIGNAL(cancelRequested()), this, SLOT(cancelEditor())); connect(editor, SIGNAL(acceptRequested()), this, SLOT(acceptEditor())); editor->resize(columnWidth(col), recordHeight()); editor->installEventFilter(this); if (editor->widget()) editor->widget()->installEventFilter(this); //store d->editors.insert(tvcol, editor); return editor; } KexiTableEdit* KexiTableScrollArea::tableEditorWidget(int col, bool ignoreMissingEditor) { return dynamic_cast(editor(col, ignoreMissingEditor)); } void KexiTableScrollArea::editorShowFocus(int row, int col) { Q_UNUSED(row); KexiDataItemInterface *edit = editor(col); if (edit) { //qDebug() << "IN"; QRect rect = cellGeometry(m_curRecord, m_curColumn); edit->showFocus(rect, isReadOnly() || m_data->column(col)->isReadOnly()); } } void KexiTableScrollArea::slotEditRequested() { createEditor(m_curRecord, m_curColumn); } void KexiTableScrollArea::reloadData() { KexiDataAwareObjectInterface::reloadData(); d->scrollAreaWidget->update(); } void KexiTableScrollArea::createEditor(int row, int col, const QString& addText, CreateEditorFlags flags) { //qDebug() << "addText:" << addText << "removeOld:" << removeOld; if (row < 0) { qWarning() << "ROW NOT SPECIFIED!" << row; return; } if (isReadOnly()) { qDebug() << "DATA IS READ ONLY!"; return; } if (m_data->column(col)->isReadOnly()) {//d->pColumnModes.at(d->numCols-1) & ColumnReadOnly) qDebug() << "COL IS READ ONLY!"; return; } if (recordEditing() >= 0 && row != recordEditing()) { if (!acceptRecordEditing()) { return; } } const bool startRecordEditing = recordEditing() == -1; //remember if we're starting row edit if (startRecordEditing) { //we're starting row editing session m_data->clearRecordEditBuffer(); setRecordEditing(row); //indicate on the vheader that we are editing: if (isInsertingEnabled() && row == recordCount()) { //we should know that we are in state "new record editing" m_newRecordEditing = true; KDbRecordData *insertItem = m_insertRecord; beginInsertItem(insertItem, row); //'insert' row editing: show another row after that: m_data->append(insertItem); //new empty 'inserting' item m_insertRecord = m_data->createItem(); endInsertItem(insertItem, row); updateWidgetContentsSize(); //refr. current and next row d->scrollAreaWidget->update(columnPos(col), recordPos(row), viewport()->width(), d->recordHeight*2); if (flags & EnsureCellVisible) { ensureVisible(columnPos(col), recordPos(row + 1) + d->recordHeight - 1, columnWidth(col), d->recordHeight); } d->verticalHeader->setOffset(verticalScrollBar()->value()); } d->verticalHeader->updateSection(row); } KexiTableEdit *editorWidget = tableEditorWidget(col); m_editor = editorWidget; if (!editorWidget) return; m_editor->setValue(*bufferedValueAt(row, col, !(flags & ReplaceOldValue)/*useDefaultValueIfPossible*/), addText, flags & ReplaceOldValue); if (m_editor->hasFocusableWidget()) { editorWidget->move(columnPos(col), recordPos(row)); editorWidget->resize(columnWidth(col), recordHeight()); editorWidget->show(); m_editor->setFocus(); } if (startRecordEditing) { m_navPanel->showEditingIndicator(true); //this will allow to enable 'next' btn //emit recordEditingStarted(row); } m_editor->installListener(this); } void KexiTableScrollArea::focusOutEvent(QFocusEvent* e) { KexiDataAwareObjectInterface::focusOutEvent(e); } bool KexiTableScrollArea::focusNextPrevChild(bool /*next*/) { return false; //special Tab/BackTab meaning } void KexiTableScrollArea::resizeEvent(QResizeEvent *e) { if (d->insideResizeEvent) return; d->insideResizeEvent = true; QScrollArea::resizeEvent(e); if ((viewport()->height() - e->size().height()) <= d->recordHeight) { slotUpdate(); triggerUpdate(); } d->insideResizeEvent = false; } void KexiTableScrollArea::showEvent(QShowEvent *e) { QScrollArea::showEvent(e); if (!d->maximizeColumnsWidthOnShow.isEmpty()) { maximizeColumnsWidth(d->maximizeColumnsWidthOnShow); d->maximizeColumnsWidthOnShow.clear(); } if (m_initDataContentsOnShow) { //full init m_initDataContentsOnShow = false; initDataContents(); } else { //just update size updateScrollAreaWidgetSize(); } updateGeometries(); //now we can ensure cell's visibility ( if there was such a call before show() ) if (d->ensureCellVisibleOnShow != QPoint(-17, -17)) { // because (-1, -1) means "current cell" ensureCellVisible(d->ensureCellVisibleOnShow.y(), d->ensureCellVisibleOnShow.x()); d->ensureCellVisibleOnShow = QPoint(-17, -17); //reset the flag } if (d->firstShowEvent) { ensureVisible(0, 0, 0, 0); // needed because for small geometries contents were moved 1/2 of row height up d->firstShowEvent = false; } updateViewportMargins(); } void KexiTableScrollArea::dragMoveEvent(QDragMoveEvent *e) { if (!hasData()) return; if (m_dropsAtRecordEnabled) { QPoint p = e->pos(); int row = recordNumberAt(p.y()); if ((p.y() % d->recordHeight) > (d->recordHeight*2 / 3)) { row++; } KDbRecordData *data = m_data->at(row); emit dragOverRecord(data, row, e); if (e->isAccepted()) { if (m_dragIndicatorLine >= 0 && m_dragIndicatorLine != row) { //erase old indicator updateRecord(m_dragIndicatorLine); } if (m_dragIndicatorLine != row) { m_dragIndicatorLine = row; updateRecord(m_dragIndicatorLine); } } else { if (m_dragIndicatorLine >= 0) { //erase old indicator updateRecord(m_dragIndicatorLine); } m_dragIndicatorLine = -1; } } else { e->accept(); } } void KexiTableScrollArea::dropEvent(QDropEvent *e) { if (!hasData()) return; if (m_dropsAtRecordEnabled) { //we're no longer dragging over the table if (m_dragIndicatorLine >= 0) { int row2update = m_dragIndicatorLine; m_dragIndicatorLine = -1; updateRecord(row2update); } QPoint p = e->pos(); int row = recordNumberAt(p.y()); if ((p.y() % d->recordHeight) > (d->recordHeight*2 / 3)) { row++; } KDbRecordData *data = m_data->at(row); KDbRecordData *newData = 0; emit droppedAtRecord(data, row, e, newData ); if (newData ) { const int realRow = (row == m_curRecord ? -1 : row); insertItem(newData , realRow); setCursorPosition(row, 0); } } } void KexiTableScrollArea::dragLeaveEvent(QDragLeaveEvent *e) { Q_UNUSED(e); if (!hasData()) return; if (m_dropsAtRecordEnabled) { //we're no longer dragging over the table if (m_dragIndicatorLine >= 0) { int row2update = m_dragIndicatorLine; m_dragIndicatorLine = -1; updateRecord(row2update); } } } void KexiTableScrollArea::updateCell(int record, int column) { // qDebug() << record << column; d->scrollAreaWidget->update(cellGeometry(record, column)); } void KexiTableScrollArea::updateCurrentCell() { updateCell(m_curRecord, m_curColumn); } void KexiTableScrollArea::updateRecord(int record) { // qDebug()<value() << recordPos(row) << viewport()->width() << rowHeight(); if (record < 0 || record >= (recordCount() + 2/* sometimes we want to refresh the row after last*/)) return; //qDebug() << horizontalScrollBar()->value() << " " << verticalScrollBar()->value(); //qDebug() << QRect( columnPos( leftcol ), recordPos(row), viewport()->width(), rowHeight() ); d->scrollAreaWidget->update(horizontalScrollBar()->value(), recordPos(record), viewport()->width(), recordHeight()); } void KexiTableScrollArea::slotColumnWidthChanged(int column, int oldSize, int newSize) { Q_UNUSED(oldSize); Q_UNUSED(newSize); updateScrollAreaWidgetSize(); d->scrollAreaWidget->update(d->horizontalHeader->offset() + columnPos(column), d->verticalHeader->offset(), viewport()->width() - columnPos(column), viewport()->height()); //qDebug() << QRect(columnPos(column), 0, viewport()->width() - columnPos(column), viewport()->height()); QWidget *editorWidget = dynamic_cast(m_editor); if (editorWidget && editorWidget->isVisible()) { editorWidget->move(columnPos(m_curColumn), recordPos(m_curRecord)); editorWidget->resize(columnWidth(m_curColumn), recordHeight()); } updateGeometries(); editorShowFocus(m_curRecord, m_curColumn); if (editorWidget && editorWidget->isVisible()) { m_editor->setFocus(); } } void KexiTableScrollArea::slotSectionHandleDoubleClicked(int section) { adjustColumnWidthToContents(section); slotColumnWidthChanged(0, 0, 0); //to update contents and redraw ensureColumnVisible(section); } void KexiTableScrollArea::setSortingEnabled(bool set) { KexiDataAwareObjectInterface::setSortingEnabled(set); d->horizontalHeader->setSortingEnabled(set); } void KexiTableScrollArea::sortColumnInternal(int col, int order) { KexiDataAwareObjectInterface::sortColumnInternal(col, order); } int KexiTableScrollArea::leftMargin() const { return verticalHeaderVisible() ? d->verticalHeader->width() : 0; } int KexiTableScrollArea::topMargin() const { //qDebug() << d->horizontalHeader->height(); return horizontalHeaderVisible() ? d->horizontalHeader->height() : 0; } void KexiTableScrollArea::updateGeometries() { const QSize ts(tableSize()); if (d->horizontalHeader->offset() && ts.width() < (d->horizontalHeader->offset() + d->horizontalHeader->width())) { horizontalScrollBar()->setValue(ts.width() - d->horizontalHeader->width()); } const int colOffset = d->columnOffset(); const int frameLeftMargin = style()->pixelMetric(QStyle::PM_FocusFrameVMargin, 0, this) + colOffset; const int frameTopMargin = style()->pixelMetric(QStyle::PM_FocusFrameHMargin, 0, this) + colOffset; d->horizontalHeader->move(leftMargin() + frameLeftMargin, frameTopMargin); d->verticalHeader->move(frameLeftMargin, d->horizontalHeader->geometry().bottom() + 1); } int KexiTableScrollArea::columnWidth(int col) const { if (!hasData()) return 0; int vcID = m_data->visibleColumnIndex(col); //qDebug() << vcID << d->horizontalHeader->sectionSize(vcID); return (vcID == -1) ? 0 : d->horizontalHeader->sectionSize(vcID); } int KexiTableScrollArea::recordHeight() const { return d->recordHeight; } int KexiTableScrollArea::columnPos(int col) const { if (!hasData()) return 0; //if this column is hidden, find first column before that is visible int c = qMin(col, (int)m_data->columnCount() - 1), vcID = 0; while (c >= 0 && (vcID = m_data->visibleColumnIndex(c)) == -1) c--; if (c < 0) return 0; if (c == col) return d->horizontalHeader->sectionPosition(vcID); return d->horizontalHeader->sectionPosition(vcID) + d->horizontalHeader->sectionSize(vcID); } int KexiTableScrollArea::recordPos(int record) const { return d->recordHeight*record; } int KexiTableScrollArea::columnNumberAt(int pos) const { if (!hasData()) return -1; const int realPos = pos - d->horizontalHeader->offset(); const int c = d->horizontalHeader->logicalIndexAt(realPos); if (c < 0) return c; return m_data->globalIndexOfVisibleColumn(c); } int KexiTableScrollArea::recordNumberAt(int pos, bool ignoreEnd) const { if (!hasData()) return -1; pos /= d->recordHeight; if (pos < 0) return 0; if ((pos >= (int)m_data->count()) && !ignoreEnd) return -1; return pos; } QRect KexiTableScrollArea::cellGeometry(int record, int column) const { return QRect(columnPos(column), recordPos(record), columnWidth(column), recordHeight()); } //#define KEXITABLEVIEW_COMBO_DEBUG QSize KexiTableScrollArea::tableSize() const { #ifdef KEXITABLEVIEW_COMBO_DEBUG if (objectName() == "KexiComboBoxPopup_tv") { qDebug() << "rowCount" << rowCount() << "\nisInsertingEnabled" << isInsertingEnabled() << "columnCount" << columnCount(); } #endif if ((recordCount() + (isInsertingEnabled() ? 1 : 0)) > 0 && columnCount() > 0) { /* qDebug() << columnPos( columnCount() - 1 ) + columnWidth( columnCount() - 1 ) << ", " << recordPos( rowCount()-1+(isInsertingEnabled()?1:0)) + d->rowHeight */ // qDebug() << m_navPanel->isVisible() <<" "<height()<<" " // << horizontalScrollBar()->sizeHint().height()<<" "<recordHeight + d->internal_bottomMargin ); #ifdef KEXITABLEVIEW_COMBO_DEBUG if (objectName() == "KexiComboBoxPopup_tv") { qDebug() << "size" << s << "\ncolumnPos(columnCount()-1)" << columnPos(columnCount() - 1) << "\ncolumnWidth(columnCount()-1)" << columnWidth(columnCount() - 1) << "\nrecordPos(rowCount()-1+(isInsertingEnabled()?1:0))" << recordPos(rowCount()-1+(isInsertingEnabled()?1:0)) << "\nd->rowHeight" << d->rowHeight << "\nd->internal_bottomMargin" << d->internal_bottomMargin; } #endif // qDebug() << rowCount()-1 <<" "<< (isInsertingEnabled()?1:0) <<" "<< rowEditing() << " " << s; #ifdef KEXITABLEVIEW_DEBUG qDebug() << s << "cw(last):" << columnWidth(columnCount() - 1); #endif return s; } return QSize(0, 0); } void KexiTableScrollArea::ensureCellVisible(int record, int column) { if (!isVisible()) { //the table is invisible: we can't ensure visibility now d->ensureCellVisibleOnShow = QPoint(record, column); return; } if (column == -1) { column = m_curColumn; } if (record == -1) { record = m_curRecord; } if (column < 0 || record < 0) { return; } //quite clever: ensure the cell is visible: QRect r(columnPos(column) - 1, recordPos(record) + (d->appearance.fullRecordSelection ? 1 : 0) - 1, columnWidth(column) + 2, recordHeight() + 2); if (navPanelWidgetVisible() && horizontalScrollBar()->isHidden()) { //a hack: for visible navigator: increase height of the visible rect 'r' r.setBottom(r.bottom() + navPanelWidget()->height()); } QSize tableSize(this->tableSize()); const int bottomBorder = r.bottom() + (isInsertingEnabled() ? recordHeight() : 0); if (!spreadSheetMode() && (tableSize.height() - bottomBorder) < recordHeight()) { // ensure the very bottom of scroll area is displayed to help the user see what's there r.moveTop(tableSize.height() - r.height() + 1); } QPoint pcenter = r.center(); #ifdef KEXITABLEVIEW_DEBUG qDebug() << pcenter.x() << pcenter.y() << (r.width() / 2) << (r.height() / 2); #endif ensureVisible(pcenter.x(), pcenter.y(), r.width() / 2, r.height() / 2); } void KexiTableScrollArea::ensureColumnVisible(int col) { if (!isVisible()) { return; } //quite clever: ensure the cell is visible: QRect r(columnPos(col == -1 ? m_curColumn : col) - 1, d->verticalHeader->offset(), columnWidth(col == -1 ? m_curColumn : col) + 2, 0); QPoint pcenter = r.center(); #ifdef KEXITABLEVIEW_DEBUG qDebug() << pcenter.x() << pcenter.y() << (r.width() / 2) << (r.height() / 2); #endif ensureVisible(pcenter.x(), pcenter.y(), r.width() / 2, r.height() / 2); } void KexiTableScrollArea::deleteCurrentRecord() { KexiDataAwareObjectInterface::deleteCurrentRecord(); ensureCellVisible(m_curRecord, -1); } KDbRecordData* KexiTableScrollArea::insertEmptyRecord(int pos) { const int previousRow = m_curRecord; KDbRecordData* data = KexiDataAwareObjectInterface::insertEmptyRecord(pos); // update header selection d->verticalHeader->setCurrentIndex( d->verticalHeader->selectionModel()->model()->index(m_curRecord, m_curColumn)); d->verticalHeader->updateSection(previousRow); d->verticalHeader->updateSection(m_curRecord); return data; } void KexiTableScrollArea::updateAfterCancelRecordEditing() { KexiDataAwareObjectInterface::updateAfterCancelRecordEditing(); m_navPanel->showEditingIndicator(false); } void KexiTableScrollArea::updateAfterAcceptRecordEditing() { KexiDataAwareObjectInterface::updateAfterAcceptRecordEditing(); m_navPanel->showEditingIndicator(false); } bool KexiTableScrollArea::getVisibleLookupValue(QVariant& cellValue, KexiTableEdit *edit, KDbRecordData *data, KDbTableViewColumn *tvcol) const { if (edit->columnInfo() && edit->columnInfo()->indexForVisibleLookupValue() != -1 && edit->columnInfo()->indexForVisibleLookupValue() < (int)data->count()) { const QVariant *visibleFieldValue = 0; if (m_currentRecord == data && m_data->recordEditBuffer()) { visibleFieldValue = m_data->recordEditBuffer()->at( tvcol->visibleLookupColumnInfo(), false/*!useDefaultValueIfPossible*/); } if (visibleFieldValue) //(use bufferedValueAt() - try to get buffered visible value for lookup field) cellValue = *visibleFieldValue; else cellValue /*txt*/ = data->at(edit->columnInfo()->indexForVisibleLookupValue()); return true; } return false; } //reimpl. void KexiTableScrollArea::removeEditor() { if (!m_editor) return; KexiDataAwareObjectInterface::removeEditor(); viewport()->setFocus(); } void KexiTableScrollArea::slotRecordRepaintRequested(KDbRecordData* data) { updateRecord(m_data->indexOf(data)); } void KexiTableScrollArea::verticalScrollBarValueChanged(int v) { KexiDataAwareObjectInterface::verticalScrollBarValueChanged(v); const QPoint posInViewport = viewport()->mapFromGlobal(QCursor::pos()) - QPoint(contentsMargins().left(), contentsMargins().top()); //qDebug() << posInViewport << contentsRect().size() - QSize(leftMargin(), topMargin()) // << QRect(QPoint(0, 0), contentsRect().size() - QSize(leftMargin(), topMargin())); const int record = recordNumberAt(posInViewport.y() + verticalScrollBar()->value()); if (record >= 0) { setHighlightedRecordNumber(record); } } #ifdef KEXI_TABLE_PRINT_SUPPORT void KexiTableScrollArea::print(QPrinter & /*printer*/ , QPrintDialog & /*printDialog*/) { int leftMargin = printer.margins().width() + 2 + d->rowHeight; int topMargin = printer.margins().height() + 2; // int bottomMargin = topMargin + ( printer.realPageSize()->height() * printer.resolution() + 36 ) / 72; int bottomMargin = 0; qDebug() << "bottom:" << bottomMargin; QPainter p(&printer); KDbRecordData *i; int width = leftMargin; for (int col = 0; col < columnCount(); col++) { p.fillRect(width, topMargin - d->rowHeight, columnWidth(col), d->rowHeight, QBrush(Qt::gray)); p.drawRect(width, topMargin - d->rowHeight, columnWidth(col), d->rowHeight); p.drawText(width, topMargin - d->rowHeight, columnWidth(col), d->rowHeight, Qt::AlignLeft | Qt::AlignVCenter, d->horizontalHeader->label(col)); width = width + columnWidth(col); } int yOffset = topMargin; int row = 0; int right = 0; for (i = m_data->first(); i; i = m_data->next()) { if (!i->isInsertItem()) { qDebug() << "row=" << row << "y=" << yOffset; int xOffset = leftMargin; for (int col = 0; col < columnCount(); col++) { qDebug() << "col=" << col << "x=" << xOffset; p.saveWorldMatrix(); p.translate(xOffset, yOffset); paintCell(&p, i, row, col, QRect(0, 0, columnWidth(col) + 1, d->rowHeight), true); p.restoreWorldMatrix(); xOffset = xOffset + columnWidth(col); right = xOffset; } row++; yOffset = topMargin + row * d->rowHeight; } if (yOffset > 900) { p.drawLine(leftMargin, topMargin, leftMargin, yOffset); p.drawLine(leftMargin, topMargin, right - 1, topMargin); printer.newPage(); yOffset = topMargin; row = 0; } } p.drawLine(leftMargin, topMargin, leftMargin, yOffset); p.drawLine(leftMargin, topMargin, right - 1, topMargin); p.end(); } #endif KDbField* KexiTableScrollArea::field(int column) const { if (!m_data || !m_data->column(column)) return 0; return m_data->column(column)->field(); } void KexiTableScrollArea::adjustColumnWidthToContents(int column) { if (!hasData()) return; if (column == -1) { const int cols = columnCount(); for (int i = 0; i < cols; i++) adjustColumnWidthToContents(i); return; } int indexOfVisibleColumn = (m_data->column(column) && m_data->column(column)->columnInfo()) ? m_data->column(column)->columnInfo()->indexForVisibleLookupValue() : -1; if (-1 == indexOfVisibleColumn) indexOfVisibleColumn = column; if (indexOfVisibleColumn < 0) return; QList::ConstIterator it(m_data->constBegin()); if (it != m_data->constEnd() && (*it)->count() <= indexOfVisibleColumn) return; KexiCellEditorFactoryItem *item = KexiCellEditorFactory::item(columnType(indexOfVisibleColumn)); if (!item) return; int maxw = horizontalHeaderVisible() ? d->horizontalHeader->preferredSectionSize(column) : 0; if (maxw == 0 && m_data->isEmpty()) return; //nothing to adjust //! \todo js: this is NOT EFFECTIVE for big data sets!!!! KexiTableEdit *ed = tableEditorWidget(column/* not indexOfVisibleColumn*/); const QFontMetrics fm(fontMetrics()); if (ed) { for (it = m_data->constBegin(); it != m_data->constEnd(); ++it) { const int wfw = ed->widthForValue((*it)->at(indexOfVisibleColumn), fm); maxw = qMax(maxw, wfw); } const bool focused = currentColumn() == column; maxw += (fm.width(" ") + ed->leftMargin() + ed->rightMargin(focused) + 2); } if (maxw < KEXITV_MINIMUM_COLUMN_WIDTH) maxw = KEXITV_MINIMUM_COLUMN_WIDTH; //not too small //qDebug() << "setColumnWidth(column=" << column // << ", indexOfVisibleColumn=" << indexOfVisibleColumn << ", width=" << maxw << " )"; setColumnWidth(column/* not indexOfVisibleColumn*/, maxw); } void KexiTableScrollArea::setColumnWidth(int column, int width) { if (columnCount() <= column || column < 0) return; d->horizontalHeader->resizeSection(column, width); editorShowFocus(m_curRecord, m_curColumn); } void KexiTableScrollArea::maximizeColumnsWidth(const QList &columnList) { if (!isVisible()) { d->maximizeColumnsWidthOnShow += columnList; return; } if (width() <= d->horizontalHeader->headerWidth()) return; //sort the list and make it unique QList cl, sortedList(columnList); qSort(sortedList); int i = -999; QList::ConstIterator it(sortedList.constBegin()), end(sortedList.constEnd()); for (; it != end; ++it) { if (i != (*it)) { cl += (*it); i = (*it); } } //resize int sizeToAdd = (width() - d->horizontalHeader->headerWidth()) / cl.count() - d->verticalHeader->width(); if (sizeToAdd <= 0) return; end = cl.constEnd(); for (it = cl.constBegin(); it != end; ++it) { int w = d->horizontalHeader->sectionSize(*it); if (w > 0) { d->horizontalHeader->resizeSection(*it, w + sizeToAdd); } } d->scrollAreaWidget->update(); editorShowFocus(m_curRecord, m_curColumn); } void KexiTableScrollArea::setColumnResizeEnabled(int column, bool set) { if (column < 0 || column >= columnCount()) { return; } d->horizontalHeader->setSectionResizeMode(column, set ? QHeaderView::Interactive : QHeaderView::Fixed); } void KexiTableScrollArea::setColumnsResizeEnabled(bool set) { d->horizontalHeader->setSectionResizeMode(set ? QHeaderView::Interactive : QHeaderView::Fixed); } bool KexiTableScrollArea::stretchLastColumn() const { return d->horizontalHeader->stretchLastSection(); } void KexiTableScrollArea::setStretchLastColumn(bool set) { if (columnCount() > 0) { setColumnResizeEnabled(columnCount() - 1, !set); } d->horizontalHeader->setStretchLastSection(set); } void KexiTableScrollArea::setEditableOnDoubleClick(bool set) { d->editOnDoubleClick = set; } bool KexiTableScrollArea::editableOnDoubleClick() const { return d->editOnDoubleClick; } bool KexiTableScrollArea::verticalHeaderVisible() const { return d->verticalHeader->isVisible(); } void KexiTableScrollArea::setVerticalHeaderVisible(bool set) { d->verticalHeader->setVisible(set); updateViewportMargins(); } void KexiTableScrollArea::updateViewportMargins() { d->viewportMargins = QMargins( leftMargin() + 1, topMargin() + 1, 0, // right 0 // bottom ); setViewportMargins(d->viewportMargins); //qDebug() << d->viewportMargins; } bool KexiTableScrollArea::horizontalHeaderVisible() const { return d->horizontalHeaderVisible; } void KexiTableScrollArea::setHorizontalHeaderVisible(bool set) { d->horizontalHeaderVisible = set; //needed because isVisible() is not always accurate d->horizontalHeader->setVisible(set); updateViewportMargins(); } void KexiTableScrollArea::triggerUpdate() { // qDebug(); d->pUpdateTimer->start(20); } void KexiTableScrollArea::setHBarGeometry(QScrollBar & hbar, int x, int y, int w, int h) { #ifdef KEXITABLEVIEW_DEBUG /*todo*/ qDebug(); #endif if (d->appearance.navigatorEnabled) { m_navPanel->setHBarGeometry(hbar, x, y, w, h); } else { hbar.setGeometry(x , y, w, h); } } void KexiTableScrollArea::setSpreadSheetMode(bool set) { KexiDataAwareObjectInterface::setSpreadSheetMode(set); d->setSpreadSheetMode(set); } int KexiTableScrollArea::validRowNumber(const QString& text) { bool ok = true; int r = text.toInt(&ok); if (!ok || r < 1) r = 1; else if (r > (recordCount() + (isInsertingEnabled() ? 1 : 0))) r = recordCount() + (isInsertingEnabled() ? 1 : 0); return r -1; } void KexiTableScrollArea::moveToRecordRequested(int record) { setFocus(); selectRecord(record); } void KexiTableScrollArea::moveToLastRecordRequested() { setFocus(); selectLastRecord(); } void KexiTableScrollArea::moveToPreviousRecordRequested() { setFocus(); selectPreviousRecord(); } void KexiTableScrollArea::moveToNextRecordRequested() { setFocus(); selectNextRecord(); } void KexiTableScrollArea::moveToFirstRecordRequested() { setFocus(); selectFirstRecord(); } void KexiTableScrollArea::copySelection() { if (m_currentRecord && m_curColumn != -1) { KexiTableEdit *edit = tableEditorWidget(m_curColumn); QVariant defaultValue; const bool defaultValueDisplayed = isDefaultValueDisplayed(m_currentRecord, m_curColumn, &defaultValue); if (edit) { QVariant visibleValue; getVisibleLookupValue(visibleValue, edit, m_currentRecord, m_data->column(m_curColumn)); edit->handleCopyAction( defaultValueDisplayed ? defaultValue : m_currentRecord->at(m_curColumn), visibleValue); } } } void KexiTableScrollArea::cutSelection() { //try to handle @ editor's level KexiTableEdit *edit = tableEditorWidget(m_curColumn); if (edit) edit->handleAction("edit_cut"); } void KexiTableScrollArea::paste() { //try to handle @ editor's level KexiTableEdit *edit = tableEditorWidget(m_curColumn); if (edit) edit->handleAction("edit_paste"); } bool KexiTableScrollArea::eventFilter(QObject *o, QEvent *e) { //don't allow to stole key my events by others: // qDebug() << "spontaneous " << e->spontaneous() << " type=" << e->type(); #ifdef KEXITABLEVIEW_DEBUG if (e->type() != QEvent::Paint && e->type() != QEvent::Leave && e->type() != QEvent::MouseMove && e->type() != QEvent::HoverMove && e->type() != QEvent::HoverEnter && e->type() != QEvent::HoverLeave) { qDebug() << e << o; } if (e->type() == QEvent::Paint) { qDebug() << "PAINT!" << static_cast(e) << static_cast(e)->rect(); } #endif if (e->type() == QEvent::KeyPress) { if (e->spontaneous()) { QKeyEvent *ke = static_cast(e); const int k = ke->key(); int mods = ke->modifiers(); //cell editor's events: //try to handle the event @ editor's level KexiTableEdit *edit = tableEditorWidget(m_curColumn); if (edit && edit->handleKeyPress(ke, m_editor == edit)) { ke->accept(); return true; } else if (m_editor && (o == dynamic_cast(m_editor) || o == m_editor->widget())) { if ((k == Qt::Key_Tab && (mods == Qt::NoModifier || mods == Qt::ShiftModifier)) || (overrideEditorShortcutNeeded(ke)) || (k == Qt::Key_Enter || k == Qt::Key_Return || k == Qt::Key_Up || k == Qt::Key_Down) || (k == Qt::Key_Left && m_editor->cursorAtStart()) || (k == Qt::Key_Right && m_editor->cursorAtEnd()) ) { //try to steal the key press from editor or it's internal widget... keyPressEvent(ke); if (ke->isAccepted()) return true; } } } } else if (e->type() == QEvent::Leave) { if ( o == d->scrollAreaWidget && d->appearance.recordMouseOverHighlightingEnabled && d->appearance.persistentSelections) { if (d->highlightedRecord != -1) { int oldRow = d->highlightedRecord; d->highlightedRecord = -1; updateRecord(oldRow); d->verticalHeader->updateSection(oldRow); const bool dontPaintNonpersistentSelectionBecauseDifferentRowHasBeenHighlighted = d->appearance.recordHighlightingEnabled && !d->appearance.persistentSelections; if (oldRow != m_curRecord && m_curRecord >= 0) { if (!dontPaintNonpersistentSelectionBecauseDifferentRowHasBeenHighlighted) { //no highlight for now: show selection again updateRecord(m_curRecord); } } } } d->recentCellWithToolTip = QPoint(-1, -1); } else if (o == viewport() && e->type() == QEvent::DragEnter) { e->accept(); } return QScrollArea::eventFilter(o, e); } void KexiTableScrollArea::setBottomMarginInternal(int pixels) { d->internal_bottomMargin = pixels; updateWidgetContentsSize(); } void KexiTableScrollArea::changeEvent(QEvent *e) { switch (e->type()) { case QEvent::PaletteChange: { d->verticalHeader->setSelectionBackgroundColor(palette().color(QPalette::Highlight)); d->horizontalHeader->setSelectionBackgroundColor(palette().color(QPalette::Highlight)); break; } default:; } QScrollArea::changeEvent(e); } const KexiTableScrollArea::Appearance& KexiTableScrollArea::appearance() const { return d->appearance; } void KexiTableScrollArea::setAppearance(const Appearance& a) { setFont(font()); //this also updates contents if (a.fullRecordSelection) { d->recordHeight -= 1; } else { d->recordHeight += 1; } if (d->verticalHeader) { d->verticalHeader->setDefaultSectionSize(d->recordHeight); } if (a.recordHighlightingEnabled) { m_updateEntireRecordWhenMovingToOtherRecord = true; } navPanelWidget()->setVisible(a.navigatorEnabled); setHorizontalScrollBarPolicy(a.navigatorEnabled ? Qt::ScrollBarAlwaysOn : Qt::ScrollBarAsNeeded); d->highlightedRecord = -1; //! @todo is setMouseTracking useful for other purposes? viewport()->setMouseTracking(a.recordMouseOverHighlightingEnabled); d->appearance = a; updateViewportMargins(); } int KexiTableScrollArea::highlightedRecordNumber() const { return d->highlightedRecord; } void KexiTableScrollArea::setHighlightedRecordNumber(int record) { if (record != -1) { record = qMin(recordCount() - 1 + (isInsertingEnabled() ? 1 : 0), record); record = qMax(0, record); } const int previouslyHighlightedRow = d->highlightedRecord; if (previouslyHighlightedRow == record) { if (previouslyHighlightedRow != -1) updateRecord(previouslyHighlightedRow); return; } d->highlightedRecord = record; if (d->highlightedRecord != -1) updateRecord(d->highlightedRecord); if (previouslyHighlightedRow != -1) updateRecord(previouslyHighlightedRow); if (m_curRecord >= 0 && (previouslyHighlightedRow == -1 || previouslyHighlightedRow == m_curRecord) && d->highlightedRecord != m_curRecord && !d->appearance.persistentSelections) { //currently selected row needs to be repainted updateRecord(m_curRecord); } } KDbRecordData *KexiTableScrollArea::highlightedRecord() const { return d->highlightedRecord == -1 ? 0 : m_data->at(d->highlightedRecord); } QScrollBar* KexiTableScrollArea::verticalScrollBar() const { return QScrollArea::verticalScrollBar(); } int KexiTableScrollArea::lastVisibleRecord() const { return recordNumberAt(verticalScrollBar()->value()); } void KexiTableScrollArea::valueChanged(KexiDataItemInterface* item) { #ifdef KEXITABLEVIEW_DEBUG qDebug() << item->field()->name() << item->value(); #else Q_UNUSED(item); #endif // force reload editing-related actions emit updateSaveCancelActions(); } bool KexiTableScrollArea::cursorAtNewRecord() const { return m_newRecordEditing; } void KexiTableScrollArea::lengthExceeded(KexiDataItemInterface *item, bool lengthExceeded) { showLengthExceededMessage(item, lengthExceeded); } void KexiTableScrollArea::updateLengthExceededMessage(KexiDataItemInterface *item) { showUpdateForLengthExceededMessage(item); } QHeaderView* KexiTableScrollArea::horizontalHeader() const { return d->horizontalHeader; } QHeaderView* KexiTableScrollArea::verticalHeader() const { return d->verticalHeader; } int KexiTableScrollArea::horizontalHeaderHeight() const { return d->horizontalHeader->height(); } QWidget* KexiTableScrollArea::navPanelWidget() const { return dynamic_cast(m_navPanel); } bool KexiTableScrollArea::navPanelWidgetVisible() const { return navPanelWidget() && d->appearance.navigatorEnabled; } bool KexiTableScrollArea::event(QEvent *e) { switch (e->type()) { case QEvent::QueryWhatsThis: case QEvent::WhatsThis: { QHelpEvent *he = static_cast(e); QString text = whatsThisText(he->pos()); if (!text.isEmpty()) { if (e->type() == QEvent::WhatsThis) { QWhatsThis::showText(mapToGlobal(he->pos()), text, this); } return true; } return false; } default: break; } return QScrollArea::event(e); } QString KexiTableScrollArea::whatsThisText(const QPoint &pos) const { const int leftMargin = verticalHeaderVisible() ? d->verticalHeader->width() : 0; if (KDbUtils::hasParent(d->verticalHeader, childAt(pos))) { return xi18nc("@info:whatsthis", "Contains a pointer to the currently selected record."); } else if (KDbUtils::hasParent(navPanelWidget(), childAt(pos))) { return xi18nc("@info:whatsthis", "Record navigator."); } const int col = columnNumberAt(pos.x() - leftMargin); KDbField *f = col == -1 ? 0 : field(col); if (!f) { return QString(); } return xi18nc("@info:whatsthis", "Column %1.", f->description().isEmpty() ? f->captionOrName() : f->description()); } void KexiTableScrollArea::selectCellInternal(int previousRow, int previousColumn) { // let the current style draw selection d->horizontalHeader->setCurrentIndex( d->horizontalHeader->selectionModel()->model()->index(m_curRecord, m_curColumn)); d->verticalHeader->setCurrentIndex( d->verticalHeader->selectionModel()->model()->index(m_curRecord, m_curColumn)); if (previousColumn != m_curColumn) { d->horizontalHeader->updateSection(previousColumn); } d->horizontalHeader->updateSection(m_curColumn); if (previousRow != m_curRecord) { d->verticalHeader->updateSection(previousRow); } d->verticalHeader->updateSection(m_curRecord); } QAbstractItemModel* KexiTableScrollArea::headerModel() const { return d->headerModel; } void KexiTableScrollArea::beginInsertItem(KDbRecordData *data, int pos) { Q_UNUSED(data); KexiTableScrollAreaHeaderModel* headerModel = static_cast(d->headerModel); headerModel->beginInsertRows(headerModel->index(pos, 0).parent(), pos, pos); } void KexiTableScrollArea::endInsertItem(KDbRecordData *data, int pos) { Q_UNUSED(data); Q_UNUSED(pos); KexiTableScrollAreaHeaderModel* headerModel = static_cast(d->headerModel); headerModel->endInsertRows(); } void KexiTableScrollArea::beginRemoveItem(KDbRecordData *data, int pos) { Q_UNUSED(data); KexiTableScrollAreaHeaderModel* headerModel = static_cast(d->headerModel); headerModel->beginRemoveRows(headerModel->index(pos, 0).parent(), pos, pos); } void KexiTableScrollArea::endRemoveItem(int pos) { Q_UNUSED(pos); KexiTableScrollAreaHeaderModel* headerModel = static_cast(d->headerModel); headerModel->endRemoveRows(); updateWidgetContentsSize(); } int KexiTableScrollArea::recordCount() const { return KexiDataAwareObjectInterface::recordCount(); } int KexiTableScrollArea::currentRecord() const { return KexiDataAwareObjectInterface::currentRecord(); } diff --git a/src/widget/tableview/KexiTableScrollArea.h b/src/widget/tableview/KexiTableScrollArea.h index b03567e86..27d94ad93 100644 --- a/src/widget/tableview/KexiTableScrollArea.h +++ b/src/widget/tableview/KexiTableScrollArea.h @@ -1,711 +1,711 @@ /* This file is part of the KDE project Copyright (C) 2002 Till Busch Copyright (C) 2003 Lucijan Busch Copyright (C) 2003 Daniel Molkentin Copyright (C) 2003 Joseph Wenninger Copyright (C) 2003-2015 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. Original Author: Till Busch Original Project: buX (www.bux.at) */ #ifndef KEXITABLESCROLLAREA_H #define KEXITABLESCROLLAREA_H #include "kexidatatable_export.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include class QPrinter; class QPrintDialog; class QAbstractItemModel; class KexiTableEdit; //! minimum column width in pixels #define KEXITV_MINIMUM_COLUMN_WIDTH 10 //! @short KexiTableScrollArea class provides a widget for displaying data in a tabular view. /*! @see KexiFormScrollView */ class KEXIDATATABLE_EXPORT KexiTableScrollArea : public QScrollArea, public KexiRecordNavigatorHandler, public KexiSharedActionClient, public KexiDataAwareObjectInterface, public KexiDataItemChangesListener { Q_OBJECT KEXI_DATAAWAREOBJECTINTERFACE public: /*! Defines table view's detailed appearance settings. */ class KEXIDATATABLE_EXPORT Appearance { public: explicit Appearance(QWidget *widget = 0); /*! Base color for cells, default is "Base" color for application's current palette */ QColor baseColor; /*! Text color for cells, default is "Text" color for application's current palette */ QColor textColor; /*! Grid color for cells, default is the same as default grid color of QTableView for application's current palette. */ QColor gridColor; /*! empty area color, default is "Base" color for application's current palette. */ QColor emptyAreaColor; /*! Alternate base color for cells, default is "AlternateBase" color for application's current palette. */ QColor alternateBaseColor; /*! True if background alternate color should be used, true by default. */ bool backgroundAltering; /*! True if full-record-selection mode is set, what means that all cells of the current record are always selected, instead of single cell. This mode is usable for read-only table views, when we're interested only in navigating by records. False by default, even for read-only table views. */ bool fullRecordSelection; /*! True if full grid is enabled. By default true if backgroundAltering is false or if baseColor == alternateBaseColor. It is set to false for comboboxpopup table, to mimic original combobox look and feel. */ bool horizontalGridEnabled; /*! True if full grid is enabled. True by default. It is set to false for comboboxpopup table, to mimic original combobox look and feel. */ bool verticalGridEnabled; /*! \if the navigation panel is enabled (visible) for the view. True by default. */ bool navigatorEnabled; /*! True if "record highlight" behaviour is enabled. False by default. */ bool recordHighlightingEnabled; /*! True if "record highlight over" behaviour is enabled. False by default. */ bool recordMouseOverHighlightingEnabled; /*! True if selection of a record should be kept when a user moved mouse pointer over other records. Makes only sense when recordMouseOverHighlightingEnabled is true. True by default. It is set to false for comboboxpopup table, to mimic original combobox look and feel. */ bool persistentSelections; /*! Color for record highlight, default is intermediate (33%/60%) between active highlight and base color. */ QColor recordHighlightingColor; /*! Color for text under record highlight, default is the same as textColor. Used when recordHighlightingEnabled is true. */ QColor recordHighlightingTextColor; /*! Color for record highlight for mouseover, default is intermediate (20%/80%) between active highlight and base color. Used when recordMouseOverHighlightingEnabled is true. */ QColor recordMouseOverHighlightingColor; /*! Color for text under record highlight for mouseover, default is the same as textColor. Used when recordMouseOverHighlightingEnabled is true. */ QColor recordMouseOverHighlightingTextColor; /*! Like recordMouseOverHighlightingColor but for areas painted with alternate color. This is computed using active highlight color and alternateBaseColor. */ QColor recordMouseOverAlternateHighlightingColor; }; explicit KexiTableScrollArea(KDbTableViewData* data = 0, QWidget* parent = 0); virtual ~KexiTableScrollArea(); //! redeclared to avoid conflict with private QWidget::data inline KDbTableViewData *data() const { return KexiDataAwareObjectInterface::data(); } /*! \return current appearance settings */ const Appearance& appearance() const; /*! Sets appearance settings. Table view is updated automatically. */ void setAppearance(const Appearance& a); /*! Convenience function. \return field object that define column \a column or NULL if there is no such column */ KDbField* field(int column) const; /*! \return maximum number of records that can be displayed per one "page" for current table view's size. */ virtual int recordsPerPage() const; /*! \return number of records in this view. */ virtual int recordCount() const; /*! \return number of the currently selected record number or -1. */ virtual int currentRecord() const; QRect cellGeometry(int record, int column) const; int columnWidth(int col) const; int recordHeight() const; int columnPos(int col) const; int recordPos(int record) const; int columnNumberAt(int pos) const; int recordNumberAt(int pos, bool ignoreEnd = false) const; /*! \return true if the last visible column takes up all the available space. @see setStretchLastColumn(bool). */ bool stretchLastColumn() const; /*! \return last record visible on the screen (counting from 0). The returned value is guaranteed to be smaller or equal to currentRecord() or -1 if there are no records. */ virtual int lastVisibleRecord() const; /*! Redraws specified cell. */ virtual void updateCell(int record, int column); /*! Redraws the current cell. Implemented after KexiDataAwareObjectInterface. */ virtual void updateCurrentCell(); /*! Redraws all cells of specified record. */ virtual void updateRecord(int record); bool editableOnDoubleClick() const; void setEditableOnDoubleClick(bool set); //! \return true if the vertical header is visible bool verticalHeaderVisible() const; //! Sets vertical header's visibility void setVerticalHeaderVisible(bool set); //! \return true if the horizontal header is visible bool horizontalHeaderVisible() const; //! Sets horizontal header's visibility void setHorizontalHeaderVisible(bool set); void updateViewportMargins(); #ifdef KEXI_TABLE_PRINT_SUPPORT // printing // void setupPrinter(KPrinter &printer, QPrintDialog &printDialog); void print(QPrinter &printer, QPrintDialog &printDialog); #endif // reimplemented for internal reasons virtual QSizePolicy sizePolicy() const; virtual QSize sizeHint() const; virtual QSize minimumSizeHint() const; /*! @return geometry of the viewport, i.e. the scrollable area, minus any scrollbars, etc. Implementation for KexiDataAwareObjectInterface. */ virtual QRect viewportGeometry() const; /*! Reimplemented to update cached fonts and record sizes for the painter. */ void setFont(const QFont &f); virtual QSize tableSize() const; void emitSelected(); //! single shot after 1ms for contents updating void triggerUpdate(); //! Initializes standard editor cell editor factories. This is called internally, once. static void initCellEditorFactories(); /*! \return highlighted record number or -1 if no record is highlighted. Makes sense if record highlighting is enabled. @see Appearance::recordHighlightingEnabled setHighlightedRecord() */ int highlightedRecordNumber() const; KDbRecordData *highlightedRecord() const; /*! \return vertical scrollbar. Implemented for KexiDataAwareObjectInterface. */ virtual QScrollBar* verticalScrollBar() const; virtual bool eventFilter(QObject *o, QEvent *e); public Q_SLOTS: virtual void setData(KDbTableViewData *data, bool owner = true) { KexiDataAwareObjectInterface::setData(data, owner); } virtual void clearColumnsInternal(bool repaint); /*! Reimplementation for KexiDataAwareObjectInterface */ virtual void setSpreadSheetMode(bool set); /*! Adjusts \a column column's width to its (current) contents. If \a column == -1, all columns' width is adjusted. */ void adjustColumnWidthToContents(int column = -1); //! Sets column width to \a width. void setColumnWidth(int column, int width); /*! If \a set is true, \a column column can be interactively resized by user. This is equivalent of QHeaderView::setResizeMode(column, QHeaderView::Interactive). If \a set is false, user cannot resize the column. This is equivalent of QHeaderView::setResizeMode(column, QHeaderView::Fixed). In both cases the column is initially resized to its default size and can be resized programatically afterwards using setColumnWidth(int, int). */ void setColumnResizeEnabled(int column, bool set); /*! If \a set is true, all columns in this table view can be interactively resized by user. This is equivalent of QHeaderView::setResizeMode(QHeaderView::Interactive). If \a set is false, user cannot resize the columns. This is equivalent of QHeaderView::setResizeMode(QHeaderView::Fixed). In both cases columns are initially resized to their default sizes and can be resized programatically afterwards using setColumnWidth(int, int). */ void setColumnsResizeEnabled(bool set); /*! Maximizes widths of columns selected by \a columnList, so the horizontal header has maximum overall width. Each selected column's width will be increased by the same value. Does nothing if \a columnList is empty or there is no free space to resize columns. If this table view is not visible, resizing will be performed on showing. */ void maximizeColumnsWidth(const QList &columnList); /*! If \a set is true the last visible column takes up all the available space ensuring that the available area is not wasted. Then it can't be resized by the user (setColumnResizeEnabled(columnCount()-1, !set) is used for that). The default value is false. @note Calling setColumnResizeEnabled(columnCount()-1, bool) can override this property. */ void setStretchLastColumn(bool set); /*! Sets highlighted record number or -1 if no record has to be highlighted. Makes sense if record highlighting is enabled. @see Appearance::recordHighlightingEnabled */ void setHighlightedRecordNumber(int record); /*! Ensures that cell at \a record and \a column is visible. If \a column is -1, current column number is used. If \a record is -1, current record number is used. \a record and \a column, if not -1, must be between 0 and recordCount()-1 (or columnCount()-1 accordingly). */ virtual void ensureCellVisible(int record, int column); /*! Ensures that column \a col is visible. If \a col is -1, current column number is used. \a col, if not -1, must be between 0 and columnCount()-1. */ virtual void ensureColumnVisible(int col); /*! Deletes currently selected record; does nothing if no record is currently selected. If record is in edit mode, editing is cancelled before deleting. */ virtual void deleteCurrentRecord(); /*! Inserts one empty record above record \a record. If \a record is -1 (the default), new record is inserted above the current record (or above 1st record if there is no current). A new item becomes current if record is -1 or if record is equal currentRecord(). This method does nothing if: -inserting flag is disabled (see isInsertingEnabled()) -read-only flag is set (see isReadOnly()) \ return inserted record's data */ virtual KDbRecordData *insertEmptyRecord(int pos = -1); /*! Used when Return key is pressed on cell or "+" nav. button is clicked. Also used when we want to continue editing a cell after "invalid value" message was displayed (in this case, \a setText is usually not empty, what means that text will be set in the cell replacing previous value). */ virtual void startEditCurrentCell(const QString& setText = QString(), CreateEditorFlags flags = DefaultCreateEditorFlags) { KexiDataAwareObjectInterface::startEditCurrentCell(setText, flags); } /*! Deletes currently selected cell's contents, if allowed. In most cases delete is not accepted immediately but "record editing" mode is just started. */ virtual void deleteAndStartEditCurrentCell() { KexiDataAwareObjectInterface::deleteAndStartEditCurrentCell(); } /*! Cancels record editing All changes made to the editing record during this current session will be undone. \return true on success or false on failure (e.g. when editor does not exist) */ virtual bool cancelRecordEditing() { return KexiDataAwareObjectInterface::cancelRecordEditing(); } /*! Accepts record editing. All changes made to the editing record during this current session will be accepted (saved). \return true if accepting was successful, false otherwise (e.g. when current record contain data that does not meet given constraints). */ virtual bool acceptRecordEditing() { return KexiDataAwareObjectInterface::acceptRecordEditing(); } /*! Specifies, if this table view automatically accepts record editing (using acceptRecordEdit()) on accepting any cell's edit (i.e. after acceptEditor()). \sa acceptsRecordEditingAfterCellAccepting() */ virtual void setAcceptsRecordEditAfterCellAccepting(bool set) { KexiDataAwareObjectInterface::setAcceptsRecordEditAfterCellAccepting(set); } /*! Specifies, if this table accepts dropping data on the records. If enabled: - dragging over record is indicated by drawing a line at bottom side of this record - dragOverRecord() signal will be emitted on dragging, -droppedAtRecord() will be emitted on dropping By default this flag is set to false. */ virtual void setDropsAtRecordEnabled(bool set) { KexiDataAwareObjectInterface::setDropsAtRecordEnabled(set); } virtual bool cancelEditor() { return KexiDataAwareObjectInterface::cancelEditor(); } virtual bool acceptEditor() { return KexiDataAwareObjectInterface::acceptEditor(); } /*! Reimplementation for KexiDataAwareObjectInterface, to react on changes of the sorting flag. */ virtual void setSortingEnabled(bool set); Q_SIGNALS: void dataSet(KDbTableViewData *data); void itemSelected(KDbRecordData *data); void cellSelected(int record, int column); void itemReturnPressed(KDbRecordData *data, int record, int column); void itemDblClicked(KDbRecordData *data, int record, int column); void itemMouseReleased(KDbRecordData *data, int record, int column); void dragOverRecord(KDbRecordData *data, int record, QDragMoveEvent* e); void droppedAtRecord(KDbRecordData *data, int record, QDropEvent *e, KDbRecordData*& newData); /*! Data has been refreshed on-screen - emitted from initDataContents(). */ void dataRefreshed(); void itemChanged(KDbRecordData *data, int record, int column); void itemChanged(KDbRecordData *data, int record, int column, const QVariant &oldValue); void itemDeleteRequest(KDbRecordData *data, int record, int column); void currentItemDeleteRequest(); //! Emitted for spreadsheet mode when an item was deleted and a new item has been appended void newItemAppendedForAfterDeletingInSpreadSheetMode(); void sortedColumnChanged(int col); //! emitted when record editing is started (for updating or inserting) void recordEditingStarted(int record); //! emitted when record editing is terminated (for updating or inserting) //! no matter if accepted or not void recordEditingTerminated(int record); //! emitted when state of 'save/cancel record changes' actions should be updated. void updateSaveCancelActions(); //! Emitted in initActions() to force reload actions //! You should remove existing actions and add them again. void reloadActions(); protected Q_SLOTS: virtual void slotDataDestroying() { KexiDataAwareObjectInterface::slotDataDestroying(); } virtual void slotRecordsDeleted(const QList &records); //! updates display after deletion of many records void slotColumnWidthChanged(int column, int oldSize, int newSize); void slotSectionHandleDoubleClicked(int section); void slotUpdate(); //! implemented because we needed this as slot virtual void sortColumnInternal(int col, int order = 0); //! receives a signal from cell editors void slotEditRequested(); /*! Reloads data for this widget. Handles KDbTableViewData::reloadRequested() signal. */ virtual void reloadData(); //! Handles KDbTableViewData::recordRepaintRequested() signal virtual void slotRecordRepaintRequested(KDbRecordData* data); //! Handles KDbTableViewData::aboutToDeleteRecord() signal. Prepares info for slotRecordDeleted(). virtual void slotAboutToDeleteRecord(KDbRecordData* data, KDbResultInfo* result, bool repaint) { KexiDataAwareObjectInterface::slotAboutToDeleteRecord(data, result, repaint); } //! Handles KDbTableViewData::recordDeleted() signal to repaint when needed. virtual void slotRecordDeleted() { KexiDataAwareObjectInterface::slotRecordDeleted(); } //! Handles KDbTableViewData::recordInserted() signal to repaint when needed. virtual void slotRecordInserted(KDbRecordData *data, bool repaint) { KexiDataAwareObjectInterface::slotRecordInserted(data, repaint); } //! Like above, not db-aware version virtual void slotRecordInserted(KDbRecordData *data, int record, bool repaint) { KexiDataAwareObjectInterface::slotRecordInserted(data, record, repaint); } /*! Handles verticalScrollBar()'s valueChanged(int) signal. Called when vscrollbar's value has been changed. */ virtual void verticalScrollBarValueChanged(int v); //! for navigator virtual void moveToRecordRequested(int record); virtual void moveToLastRecordRequested(); virtual void moveToPreviousRecordRequested(); virtual void moveToNextRecordRequested(); virtual void moveToFirstRecordRequested(); virtual void addNewRecordRequested() { KexiDataAwareObjectInterface::addNewRecordRequested(); } protected: /*! Reimplementation for KexiDataAwareObjectInterface Initializes data contents (resizes it, sets cursor at 1st row). Called on setData(). Also called once on show event after reloadRequested() signal was received from KDbTableViewData object. */ virtual void initDataContents(); /*! Implementation for KexiDataAwareObjectInterface. Updates widget's contents size using QScrollView::resizeContents() depending on tableSize(). */ virtual void updateWidgetContentsSize(); /*! Reimplementation for KexiDataAwareObjectInterface */ virtual void clearVariables(); /*! Implementation for KexiDataAwareObjectInterface */ - virtual Qt::SortOrder currentLocalSortOrder() const; + virtual KDbOrderByColumn::SortOrder currentLocalSortOrder() const; /*! Implementation for KexiDataAwareObjectInterface */ virtual int currentLocalSortColumn() const; /*! Implementation for KexiDataAwareObjectInterface */ - virtual void setLocalSortOrder(int column, Qt::SortOrder order); + virtual void setLocalSortOrder(int column, KDbOrderByColumn::SortOrder order); /*! Implementation for KexiDataAwareObjectInterface */ virtual void updateGUIAfterSorting(int previousRow); int leftMargin() const; int topMargin() const; //! temporary int bottomMargin() const { return 0; } /*! @internal \return true if the row defined by \a data has default value at column \a col. If this is the case and \a value is not NULL, *value is set to the default value. */ bool isDefaultValueDisplayed(KDbRecordData *data, int col, QVariant* value = 0); //! painting and layout void drawContents(QPainter *p); void paintCell(QPainter* p, KDbRecordData *data, int record, int column, const QRect &cr, bool print = false); void paintEmptyArea(QPainter *p, int cx, int cy, int cw, int ch); void updateGeometries(); QPoint contentsToViewport2(const QPoint &p); void contentsToViewport2(int x, int y, int& vx, int& vy); QPoint viewportToContents2(const QPoint& vp); // event handling void contentsMousePressEvent(QMouseEvent* e); void contentsMouseReleaseEvent(QMouseEvent* e); //! @internal called by contentsMouseOrEvent() contentsMouseReleaseEvent() to move cursor bool handleContentsMousePressOrRelease(QMouseEvent* e, bool release); void contentsMouseMoveEvent(QMouseEvent* e); void contentsMouseDoubleClickEvent(QMouseEvent* e); void contentsContextMenuEvent(QContextMenuEvent* e); virtual void keyPressEvent(QKeyEvent *e); //virtual void focusInEvent(QFocusEvent* e); virtual void focusOutEvent(QFocusEvent* e); virtual void resizeEvent(QResizeEvent* e); //virtual void viewportResizeEvent(QResizeEvent *e); virtual void showEvent(QShowEvent *e); virtual void dragMoveEvent(QDragMoveEvent *e); virtual void dropEvent(QDropEvent *e); virtual void dragLeaveEvent(QDragLeaveEvent *e); //! For handling changes of palette virtual void changeEvent(QEvent *e); /*! Implementation for KexiDataAwareObjectInterface */ virtual KexiDataItemInterface *editor(int col, bool ignoreMissingEditor = false); KexiTableEdit *tableEditorWidget(int col, bool ignoreMissingEditor = false); /*! Implementation for KexiDataAwareObjectInterface */ virtual void editorShowFocus(int row, int col); //! Creates editors and shows it, what usually means the beginning of a cell editing virtual void createEditor(int row, int col, const QString& addText = QString(), CreateEditorFlags flags = DefaultCreateEditorFlags); bool focusNextPrevChild(bool next); /*! Used in key event: \return true if event \a e should execute action \a action_name. Action shortcuts defined by shortCutPressed() are reused, if present, and if \a e matches given action's shortcut - false is returned (beause action is already performed at main window's level). */ bool shortCutPressed(QKeyEvent *e, const QString &action_name); /*! Shows context menu at \a pos for selected cell if menu is configured, else: contextMenuRequested() signal is emitted. Method used in contentsMousePressEvent() (for right button) and keyPressEvent() for Qt::Key_Menu key. If \a pos is QPoint(-1,-1) (the default), menu is positioned below the current cell. */ void showContextMenu(const QPoint& pos = QPoint(-1, -1)); /*! internal */ inline void paintRow(KDbRecordData *data, QPainter *pb, int r, int rowp, int cx, int cy, int colfirst, int collast, int maxwc); virtual void setHBarGeometry(QScrollBar & hbar, int x, int y, int w, int h); //! Setups navigator widget void setupNavigator(); //! internal, to determine valid row number when navigator text changed int validRowNumber(const QString& text); /*! Reimplementation for KexiDataAwareObjectInterface (viewport()->setFocus() is just added) */ virtual void removeEditor(); /*! @internal Changes bottom margin settings, in pixels. At this time, it's used by KexiComboBoxPopup to decrease margin for popup's table. */ void setBottomMarginInternal(int pixels); virtual void updateWidgetContents() { update(); } //! Copy current selection to a clipboard (e.g. cell) virtual void copySelection(); //! Cut current selection to a clipboard (e.g. cell) virtual void cutSelection(); //! Paste current clipboard contents (e.g. to a cell) virtual void paste(); /*! Used in KexiDataAwareObjectInterface::slotRecordDeleted() to repaint tow \a row and all visible below. */ virtual void updateAllVisibleRecordsBelow(int row); void updateAfterCancelRecordEditing(); void updateAfterAcceptRecordEditing(); /*! Sets \a cellValue if there is a lookup value for the cell \a data. Used in paintCell() and KexiTableCellToolTip::maybeTip() \return true if \a cellValue has been found. */ bool getVisibleLookupValue(QVariant& cellValue, KexiTableEdit *edit, KDbRecordData *data, KDbTableViewColumn *tvcol) const; /*! Implementation for KexiDataItemChangesListener. Reaction for change of \a item. */ virtual void valueChanged(KexiDataItemInterface* item); /*! Implementation for KexiDataItemChangesListener. */ virtual bool cursorAtNewRecord() const; /*! Implementation for KexiDataItemChangesListener. */ virtual void lengthExceeded(KexiDataItemInterface *item, bool lengthExceeded); /*! Implementation for KexiDataItemChangesListener. */ virtual void updateLengthExceededMessage(KexiDataItemInterface *item); virtual int horizontalHeaderHeight() const; //! @return record navigator pane QWidget* navPanelWidget() const; //! @return true if record navigator pane exists and is has "visible" appearance set to ON bool navPanelWidgetVisible() const; virtual bool event(QEvent *e); //! @internal @return text information about a given column or other specific areas of the table view. QString whatsThisText(const QPoint &pos) const; /*! Called by KexiDataAwareObjectInterface::setCursorPosition() if cursor's position is really changed. */ virtual void selectCellInternal(int previousRow, int previousColumn); //! @return horizontal header virtual QHeaderView* horizontalHeader() const; //! @return vertical header virtual QHeaderView* verticalHeader() const; //! @return common model for header views of this area. @see KexiTableScrollAreaHeader QAbstractItemModel* headerModel() const; void updateScrollAreaWidgetSize(); //! Update section of vertical header virtual void updateVerticalHeaderSection(int section); virtual void beginInsertItem(KDbRecordData *data, int pos); virtual void endInsertItem(KDbRecordData *data, int pos); virtual void beginRemoveItem(KDbRecordData *data, int pos); virtual void endRemoveItem(int pos); class Private; Private * const d; friend class KexiTableScrollAreaWidget; friend class KexiTableCellToolTip; friend class KexiTableScrollAreaHeader; }; #endif