1
/* This file is part of the KDE project
2
Copyright (C) 2002 Peter Simonsson <psn@linux.se>
3
Copyright (C) 2003-2016 JarosÅaw Staniek <staniek@kde.org>
5
This program is free software; you can redistribute it and/or
6
modify it under the terms of the GNU Library General Public
7
License as published by the Free Software Foundation; either
8
version 2 of the License, or (at your option) any later version.
10
This program is distributed in the hope that it will be useful,
11
but WITHOUT ANY WARRANTY; without even the implied warranty of
12
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13
Library General Public License for more details.
15
You should have received a copy of the GNU Library General Public License
16
along with this program; see the file COPYING. If not, write to
17
the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
18
* Boston, MA 02110-1301, USA.
22
#include "kexicomboboxbase.h"
23
#include <kexi_global.h>
24
#include <widget/utils/kexicomboboxdropdownbutton.h>
25
#include "kexicomboboxpopup.h"
26
#include "KexiTableScrollArea.h"
28
#include "KexiTableScrollAreaWidget.h"
30
#include <KDbTableSchema>
31
#include <KDbTableViewColumn>
33
#include <QApplication>
34
#include <QDesktopWidget>
35
#include <QScopedValueRollback>
39
KexiComboBoxBase::KexiComboBoxBase()
41
m_internalEditorValueChanged = false; //user has text or other value inside editor
42
m_slotInternalEditorValueChanged_enabled = true;
43
m_mouseBtnPressedWhenPopupVisible = false;
44
m_insideCreatePopup = false;
45
m_setValueOrTextInInternalEditor_enabled = true;
46
m_updatePopupSelectionOnShow = true;
47
m_moveCursorToEndInInternalEditor_enabled = true;
48
m_selectAllInInternalEditor_enabled = true;
49
m_setValueInInternalEditor_enabled = true;
50
m_setVisibleValueOnSetValueInternal = false;
51
m_reinstantiatePopupOnShow = false;
52
m_focusPopupBeforeShow = false;
55
KexiComboBoxBase::~KexiComboBoxBase()
59
KDbLookupFieldSchema *KexiComboBoxBase::lookupFieldSchema() const
61
if (field() && field()->table()) {
62
KDbLookupFieldSchema *lookupFieldSchema = field()->table()->lookupFieldSchema(*field());
63
if (lookupFieldSchema && !lookupFieldSchema->recordSource().name().isEmpty())
64
return lookupFieldSchema;
69
int KexiComboBoxBase::recordToHighlightForLookupTable() const
73
KDbLookupFieldSchema *lookupFieldSchema = this->lookupFieldSchema();
74
if (!lookupFieldSchema)
76
if (lookupFieldSchema->boundColumn() == -1)
79
const int recordUid = origValue().toInt();
80
//! @todo for now we're assuming the id is INTEGER
81
KDbTableViewData *tvData = popup()->tableView()->data();
82
const int boundColumn = boundColumnIndex();
83
if (boundColumn < 0) {
87
for (KDbTableViewDataIterator it(tvData->begin()); it != tvData->end(); ++it) {
89
KDbRecordData* data = *it;
90
if (data->at(boundColumn).toInt(&ok) == recordUid && ok)
95
//item not found: highlight 1st record, if available
99
void KexiComboBoxBase::setValueInternal(const QVariant& add_, bool removeOld)
102
m_mouseBtnPressedWhenPopupVisible = false;
103
m_updatePopupSelectionOnShow = true;
104
QString add(add_.toString());
106
KDbTableViewData *relData = column() ? column()->relatedData() : 0;
108
bool hasValueToSet = true;
109
int recordToHighlight = -1;
110
KDbLookupFieldSchema *lookupFieldSchema = this->lookupFieldSchema();
111
if (lookupFieldSchema) {
112
//use 'lookup field' model
113
//! @todo support more RowSourceType's, not only table
114
if (lookupFieldSchema->boundColumn() == -1)
117
if (m_setVisibleValueOnSetValueInternal) {
118
//only for table views
120
createPopup(false/*!show*/);
123
const int recordToHighlight = recordToHighlightForLookupTable();
124
popup()->tableView()->setHighlightedRecordNumber(recordToHighlight);
126
const int visibleColumn = visibleColumnIndex();
127
if (m_setVisibleValueOnSetValueInternal && -1 != visibleColumn) {
128
//only for table views
129
KDbRecordData *data = popup()->tableView()->highlightedRecord();
131
valueToSet = data->at(visibleColumn);
133
hasValueToSet = false;
136
} else if (relData) {
137
//use 'related table data' model
138
valueToSet = valueForString(origValue().toString(), &recordToHighlight, 0, 1);
140
//use 'enum hints' model
141
const int record = origValue().toInt();
142
valueToSet = field()->enumHint(record).trimmed();
145
setValueOrTextInInternalEditor(valueToSet);
146
/*impl.*/moveCursorToEndInInternalEditor();
147
/*impl.*/selectAllInInternalEditor();
150
if (origValue().isNull()) {
151
popup()->tableView()->clearSelection();
152
popup()->tableView()->setHighlightedRecordNumber(0);
155
if (recordToHighlight != -1)
156
popup()->tableView()->setHighlightedRecordNumber(recordToHighlight);
157
} else if (!lookupFieldSchema) {
158
//popup()->tableView()->selectRecord(origValue().toInt());
159
popup()->tableView()->setHighlightedRecordNumber(origValue().toInt());
164
//! @todo autocompl.?
166
popup()->tableView()->clearSelection();
167
/*impl.*/setValueInInternalEditor(add); //not setLineEditText(), because 'add' is entered by user!
168
//setLineEditText( add );
169
/*impl.*/moveCursorToEndInInternalEditor();
173
KDbRecordData* KexiComboBoxBase::selectRecordForEnteredValueInLookupTable(const QVariant& v)
175
KDbLookupFieldSchema *lookupFieldSchema = this->lookupFieldSchema();
176
if (!popup() || !lookupFieldSchema)
178
//-not effective for large sets: please cache it!
179
//.trimmed() is not generic!
181
const bool valueIsText = v.type() == QVariant::String || v.type() == QVariant::ByteArray; //most common case
182
const QString txt(valueIsText ? v.toString().trimmed() : QString());
183
KDbTableViewData *lookupData = popup()->tableView()->data();
184
const int visibleColumn = visibleColumnIndex();
185
if (-1 == visibleColumn)
187
KDbTableViewDataConstIterator it(lookupData->constBegin());
189
for (record = 0;it != lookupData->constEnd();++it, record++) {
191
if ((*it)->at(visibleColumn).toString().trimmed().compare(txt, Qt::CaseInsensitive) == 0)
194
if ((*it)->at(visibleColumn) == v)
199
m_setValueOrTextInInternalEditor_enabled = false; // <-- this is the entered value,
200
// so do not change the internal editor's contents
201
if (it != lookupData->constEnd())
202
popup()->tableView()->selectRecord(record);
204
popup()->tableView()->clearSelection();
206
m_setValueOrTextInInternalEditor_enabled = true;
208
return it != lookupData->constEnd() ? *it : 0;
211
QString KexiComboBoxBase::valueForString(const QString& str, int* record,
212
int lookInColumn, int returnFromColumn, bool allowNulls)
214
Q_UNUSED(returnFromColumn);
215
KDbTableViewData *relData = column() ? column()->relatedData() : 0;
217
return QString(); //safety
218
//use 'related table data' model
219
//-not effective for large sets: please cache it!
220
//.trimmed() is not generic!
222
const QString txt(str.trimmed());
223
KDbTableViewDataIterator it(relData->begin());
224
for (*record = 0;it != relData->end();++it, (*record)++) {
225
const QString s((*it)->at(lookInColumn).toString());
226
if (s.trimmed().compare(txt, Qt::CaseInsensitive) == 0)
232
if (column() && column()->isRelatedDataEditable())
233
return str; //new value entered and that's allowed
235
qWarning() << "no related record found, ID will be painted!";
238
return str; //for sanity but it's weird to show id to the user
241
int KexiComboBoxBase::boundColumnIndex() const
243
if (!lookupFieldSchema()) {
246
switch (lookupFieldSchema()->recordSource().type()) {
247
case KDbLookupFieldSchema::RecordSource::Table:
248
// When the record source is Table we have hardcoded columns: <visible>, <bound>
249
return lookupFieldSchema()->visibleColumns().count();
252
// When the record source is Query we use the lookup field's bound column index
253
//! @todo Implement for other types
254
return lookupFieldSchema()->boundColumn();
257
int KexiComboBoxBase::visibleColumnIndex() const
259
if (!lookupFieldSchema() || lookupFieldSchema()->visibleColumns().isEmpty()) {
262
switch (lookupFieldSchema()->recordSource().type()) {
263
case KDbLookupFieldSchema::RecordSource::Table:
264
// When the record source is Table we have hardcoded columns: <visible>, <bound>
265
return lookupFieldSchema()->visibleColumn(0);
268
// When the record source is Query we use the lookup field's visible column index
269
//! @todo Implement for multiple visible columns
270
//! @todo Implement for other types
271
return lookupFieldSchema()->visibleColumns().first();
274
QVariant KexiComboBoxBase::value()
276
KDbTableViewData *relData = column() ? column()->relatedData() : 0;
277
KDbLookupFieldSchema *lookupFieldSchema = 0;
279
if (m_internalEditorValueChanged) {
280
//we've user-entered text: look for id
281
//! @todo make error if matching text not found?
282
int recordToHighlight;
283
return valueForString(m_userEnteredValue.toString(), &recordToHighlight, 1, 0, true/*allowNulls*/);
285
//use 'related table data' model
286
KDbRecordData *data = popup() ? popup()->tableView()->selectedRecord() : 0;
287
return data ? data->at(0) : origValue();
289
} else if ((lookupFieldSchema = this->lookupFieldSchema())) {
290
if (lookupFieldSchema->boundColumn() == -1)
292
KDbRecordData *data = popup() ? popup()->tableView()->selectedRecord() : 0;
293
if (/*!record &&*/ m_internalEditorValueChanged && !m_userEnteredValue.toString().isEmpty()) { //
294
//try to select a record using the user-entered text
296
QVariant prevUserEnteredValue = m_userEnteredValue;
298
m_userEnteredValue = prevUserEnteredValue;
300
data = selectRecordForEnteredValueInLookupTable(m_userEnteredValue);
302
const int boundColumn = boundColumnIndex();
303
return (data && boundColumn >= 0) ? data->at(boundColumn) : QVariant();
304
} else if (popup()) {
305
//use 'enum hints' model
306
const int record = popup()->tableView()->currentRecord();
308
return QVariant(record);
311
if (valueFromInternalEditor().toString().isEmpty())
313
/*! \todo don't return just 1st record, but use autocompletion feature
314
and: show message box if entered text does not match! */
315
return origValue(); //unchanged
318
QVariant KexiComboBoxBase::visibleValueForLookupField()
320
KDbLookupFieldSchema *lookupFieldSchema = this->lookupFieldSchema();
321
if (!popup() || !lookupFieldSchema)
323
const int visibleColumn = visibleColumnIndex();
324
//qDebug() << "visibleColumn" << visibleColumn;
325
if (-1 == visibleColumn)
327
KDbRecordData *data = popup()->tableView()->selectedRecord();
328
return data ? data->at(qMin(visibleColumn, data->count() - 1)/*sanity*/) : QVariant();
331
QVariant KexiComboBoxBase::visibleValue()
333
return m_visibleValue;
336
void KexiComboBoxBase::clear()
340
slotInternalEditorValueChanged(QVariant());
343
tristate KexiComboBoxBase::valueChangedInternal()
345
//avoid comparing values:
346
KDbTableViewData *relData = column() ? column()->relatedData() : 0;
347
KDbLookupFieldSchema *lookupFieldSchema = this->lookupFieldSchema();
348
if (relData || lookupFieldSchema) {
349
if (m_internalEditorValueChanged)
352
//use 'related table data' model
353
KDbRecordData *data = popup() ? popup()->tableView()->selectedRecord() : 0;
357
//use 'enum hints' model
358
const int record = popup() ? popup()->tableView()->currentRecord() : -1;
359
if (record < 0 && !m_internalEditorValueChanged/*true if text box is cleared*/)
366
bool KexiComboBoxBase::valueIsNull()
371
// return !ok || v.isNull();
374
bool KexiComboBoxBase::valueIsEmpty()
376
return valueIsNull();
379
void KexiComboBoxBase::showPopup()
385
void KexiComboBoxBase::createPopup(bool show)
387
//qDebug() << show << field() << popup() << m_updatePopupSelectionOnShow;
390
QWidget* thisWidget = dynamic_cast<QWidget*>(this);
394
QScopedValueRollback<bool> insideCreatePopuRollback(m_insideCreatePopup, true);
395
QWidget *widgetToFocus = internalEditor() ? internalEditor() : thisWidget;
396
//qDebug() << "widgetToFocus:" << widgetToFocus;
398
if (m_reinstantiatePopupOnShow) {
399
QWidget *oldPopup = popup();
405
setPopup(column() ? new KexiComboBoxPopup(thisWidget, *column())
406
: new KexiComboBoxPopup(thisWidget, *field()));
407
QObject::connect(popup(), SIGNAL(recordAccepted(KDbRecordData*,int)),
408
thisWidget, SLOT(slotRecordAccepted(KDbRecordData*,int)));
409
QObject::connect(popup()->tableView(), SIGNAL(itemSelected(KDbRecordData*)),
410
thisWidget, SLOT(slotRecordSelected(KDbRecordData*)));
412
popup()->setFocusProxy(widgetToFocus);
413
popup()->tableView()->setFocusProxy(widgetToFocus);
414
popup()->installEventFilter(thisWidget);
416
if (origValue().isNull())
417
popup()->tableView()->clearSelection();
419
popup()->tableView()->selectRecord(0);
420
popup()->tableView()->setHighlightedRecordNumber(0);
423
if (show && internalEditor() && !internalEditor()->isVisible())
424
/*emit*/editRequested();
426
QPoint posMappedToGlobal = mapFromParentToGlobal(thisWidget->pos());
427
if (posMappedToGlobal != QPoint(-1, -1)) {
428
//! todo alter the position to fit the popup within screen boundaries
429
QPoint pos = posMappedToGlobal + QPoint(0, thisWidget->height());
430
if (qobject_cast<KexiTableScrollAreaWidget*>(thisWidget->parentWidget())) {
431
KexiTableScrollArea* tableScroll
432
= qobject_cast<KexiTableScrollAreaWidget*>(thisWidget->parentWidget())->scrollArea;
433
pos -= QPoint(tableScroll->horizontalScrollBar()->value(),
434
tableScroll->verticalScrollBar()->value());
438
//qDebug() << "pos:" << posMappedToGlobal + QPoint(0, thisWidget->height());
439
//to avoid flickering: first resize to 0-height, then show and resize back to prev. height
440
int w = popupWidthHint();
441
popup()->resize(w, 0);
444
//qDebug() << "SHOW!!!";
446
popup()->updateSize(w);
448
// make sure the popup fits on the screen
449
const QRect screen = QApplication::desktop()->availableGeometry(posMappedToGlobal);
450
pos -= screen.topLeft(); // to simplify computation
451
w = popup()->width();
452
int h = popup()->height();
453
if (screen.width() < w) {
456
} else if (screen.width() < (pos.x() + w - 1)) {
457
pos.setX(screen.width() - w + 1);
458
} else if (pos.x() < 0) {
461
if (screen.height() < h) {
464
} else if (screen.height() < (pos.y() + h - 1)) {
465
const int topY = pos.y() - thisWidget->height() - h;
466
if (topY >= 0 && (topY + h - 1 < screen.height())) {
467
pos.setY(pos.y() - thisWidget->height() - h);
469
pos.setY(screen.height() - h + 1);
471
} else if (pos.y() < 0) {
474
popup()->move(pos + screen.topLeft());
475
popup()->resize(w, h);
477
if (m_updatePopupSelectionOnShow) {
478
int recordToHighlight = -1;
479
KDbLookupFieldSchema *lookupFieldSchema = this->lookupFieldSchema();
480
KDbTableViewData *relData = column() ? column()->relatedData() : 0;
481
if (lookupFieldSchema) {
482
recordToHighlight = recordToHighlightForLookupTable();
483
} else if (relData) {
484
(void)valueForString(origValue().toString(), &recordToHighlight, 0, 1);
486
recordToHighlight = origValue().toInt();
488
/*-->*/ m_moveCursorToEndInInternalEditor_enabled = show;
489
m_selectAllInInternalEditor_enabled = show;
490
m_setValueInInternalEditor_enabled = show;
491
if (recordToHighlight == -1) {
492
recordToHighlight = qMax(popup()->tableView()->highlightedRecordNumber(), 0);
493
setValueInInternalEditor(QVariant());
495
popup()->tableView()->selectRecord(recordToHighlight);
496
popup()->tableView()->setHighlightedRecordNumber(recordToHighlight);
497
popup()->tableView()->ensureCellVisible(-1, 0); // scroll to left as expected
499
/*-->*/ m_moveCursorToEndInInternalEditor_enabled = true;
500
m_selectAllInInternalEditor_enabled = true;
501
m_setValueInInternalEditor_enabled = true;
506
moveCursorToEndInInternalEditor();
507
selectAllInInternalEditor();
508
if (m_focusPopupBeforeShow) {
509
widgetToFocus->setFocus();
514
if (!m_focusPopupBeforeShow) {
515
widgetToFocus->setFocus();
520
void KexiComboBoxBase::hide()
526
void KexiComboBoxBase::slotRecordAccepted(KDbRecordData* data, int record)
532
slotRecordSelected(data);
533
/*emit*/acceptRequested();
536
void KexiComboBoxBase::acceptPopupSelection()
540
KDbRecordData *data = popup()->tableView()->highlightedRecord();
542
popup()->tableView()->selectRecord(popup()->tableView()->highlightedRecordNumber());
543
slotRecordAccepted(data, -1);
548
void KexiComboBoxBase::slotRecordSelected(KDbRecordData*)
550
//qDebug() << "m_visibleValue=" << m_visibleValue;
553
KDbTableViewData *relData = column() ? column()->relatedData() : 0;
554
KDbLookupFieldSchema *lookupFieldSchema = this->lookupFieldSchema();
556
m_visibleValue = lookupFieldSchema ? visibleValueForLookupField() : QVariant();
559
//use 'related table data' model
560
KDbRecordData *data = popup()->tableView()->selectedRecord();
562
valueToSet = data->at(1);
563
} else if (lookupFieldSchema) {
564
KDbRecordData *data = popup()->tableView()->selectedRecord();
565
const int visibleColumn = visibleColumnIndex();
566
if (data && visibleColumn != -1 /* && (int)item->size() >= visibleColumn --already checked*/) {
567
valueToSet = data->at(qMin(visibleColumn, data->count() - 1)/*sanity*/);
568
popup()->tableView()->ensureCellVisible(popup()->tableView()->highlightedRecordNumber(), -1);
571
//use 'enum hints' model
572
valueToSet = field()->enumHint(popup()->tableView()->currentRecord());
573
if (valueToSet.toString().isEmpty() && !m_insideCreatePopup) {
575
QWidget* thisWidget = dynamic_cast<QWidget*>(this);
577
thisWidget->parentWidget()->setFocus();
582
setValueOrTextInInternalEditor(valueToSet);
583
QWidget* thisWidget = dynamic_cast<QWidget*>(this);
584
thisWidget->setFocus();
585
if (m_setValueOrTextInInternalEditor_enabled) {
586
moveCursorToEndInInternalEditor();
587
selectAllInInternalEditor();
589
// a new (temp) popup table index is selected: do not update selection next time:
590
m_updatePopupSelectionOnShow = false;
593
void KexiComboBoxBase::slotInternalEditorValueChanged(const QVariant& v)
595
if (!m_slotInternalEditorValueChanged_enabled)
597
m_userEnteredValue = v;
598
m_internalEditorValueChanged = true;
599
if (v.toString().isEmpty()) {
601
popup()->tableView()->clearSelection();
607
void KexiComboBoxBase::setValueOrTextInInternalEditor(const QVariant& value)
609
if (!m_setValueOrTextInInternalEditor_enabled)
611
setValueInInternalEditor(value);
612
//this text is not entered by hand:
613
m_userEnteredValue = QVariant();
614
m_internalEditorValueChanged = false;
617
bool KexiComboBoxBase::handleKeyPressForPopup(QKeyEvent *ke)
619
const int k = ke->key();
620
int highlightedOrSelectedRecord = popup() ? popup()->tableView()->highlightedRecordNumber() : -1;
621
if (popup() && highlightedOrSelectedRecord < 0)
622
highlightedOrSelectedRecord = popup()->tableView()->currentRecord();
624
const bool enterPressed = k == Qt::Key_Enter || k == Qt::Key_Return;
626
// The editor may be active but the pull down menu not existent/visible,
627
// e.g. when the user has pressed a normal button to activate the editor
628
// Don't handle the event here in that case.
629
if (!popup() || (!enterPressed && !popup()->isVisible())) {
635
popup()->tableView()->setHighlightedRecordNumber(
636
qMax(highlightedOrSelectedRecord - 1, 0));
637
updateTextForHighlightedRecord();
640
popup()->tableView()->setHighlightedRecordNumber(
641
qMin(highlightedOrSelectedRecord + 1, popup()->tableView()->recordCount() - 1));
642
updateTextForHighlightedRecord();
645
popup()->tableView()->setHighlightedRecordNumber(
646
qMax(highlightedOrSelectedRecord - popup()->tableView()->recordsPerPage(), 0));
647
updateTextForHighlightedRecord();
649
case Qt::Key_PageDown:
650
popup()->tableView()->setHighlightedRecordNumber(
651
qMin(highlightedOrSelectedRecord + popup()->tableView()->recordsPerPage(),
652
popup()->tableView()->recordCount() - 1));
653
updateTextForHighlightedRecord();
656
popup()->tableView()->setHighlightedRecordNumber(0);
657
updateTextForHighlightedRecord();
660
popup()->tableView()->setHighlightedRecordNumber(popup()->tableView()->recordCount() - 1);
661
updateTextForHighlightedRecord();
664
case Qt::Key_Return: //accept
665
//select record that is highlighted
666
if (popup()->tableView()->highlightedRecordNumber() >= 0) {
667
popup()->tableView()->selectRecord(popup()->tableView()->highlightedRecordNumber());
668
acceptPopupSelection();
676
void KexiComboBoxBase::updateTextForHighlightedRecord()
678
KDbRecordData* data = popup() ? popup()->tableView()->highlightedRecord() : 0;
680
slotRecordSelected(data);
683
void KexiComboBoxBase::undoChanges()
685
KDbLookupFieldSchema *lookupFieldSchema = this->lookupFieldSchema();
686
if (lookupFieldSchema) {
687
// qDebug() << "m_visibleValue BEFORE=" << m_visibleValue;
689
popup()->tableView()->selectRecord(popup()->tableView()->highlightedRecordNumber());
690
m_visibleValue = visibleValueForLookupField();
691
// qDebug() << "m_visibleValue AFTER=" << m_visibleValue;
692
setValueOrTextInInternalEditor(m_visibleValue);