You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1664 lines
49 KiB
1664 lines
49 KiB
/* This file is part of the KDE project
|
|
Copyright (C) 2005-2006 Jaroslaw Staniek <js@iidea.pl>
|
|
|
|
This work is based on kspread/dialogs/kspread_dlg_csv.cpp
|
|
and will be merged back with KOffice libraries.
|
|
|
|
Copyright (C) 2002-2003 Norbert Andres <nandres@web.de>
|
|
Copyright (C) 2002-2003 Ariya Hidayat <ariya@kde.org>
|
|
Copyright (C) 2002 Laurent Montel <montel@kde.org>
|
|
Copyright (C) 1999 David Faure <faure@kde.org>
|
|
|
|
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 <tqbuttongroup.h>
|
|
#include <tqcheckbox.h>
|
|
#include <tqclipboard.h>
|
|
#include <tqlabel.h>
|
|
#include <tqlineedit.h>
|
|
#include <tqmime.h>
|
|
#include <tqpushbutton.h>
|
|
#include <tqradiobutton.h>
|
|
#include <tqtable.h>
|
|
#include <tqlayout.h>
|
|
#include <tqfiledialog.h>
|
|
#include <tqpainter.h>
|
|
#include <tqtextcodec.h>
|
|
#include <tqtimer.h>
|
|
#include <tqfontmetrics.h>
|
|
#include <tqtooltip.h>
|
|
|
|
#include <tdeapplication.h>
|
|
#include <kdebug.h>
|
|
#include <kdialogbase.h>
|
|
#include <tdefiledialog.h>
|
|
#include <tdelocale.h>
|
|
#include <tdemessagebox.h>
|
|
#include <tdeglobalsettings.h>
|
|
#include <kiconloader.h>
|
|
#include <kcharsets.h>
|
|
#include <knuminput.h>
|
|
#include <kprogress.h>
|
|
#include <kactivelabel.h>
|
|
|
|
#include <kexiutils/identifier.h>
|
|
#include <kexiutils/utils.h>
|
|
#include <core/kexi.h>
|
|
#include <core/kexiproject.h>
|
|
#include <core/kexipart.h>
|
|
#include <core/kexipartinfo.h>
|
|
#include <core/keximainwindow.h>
|
|
#include <core/kexiguimsghandler.h>
|
|
#include <kexidb/connection.h>
|
|
#include <kexidb/tableschema.h>
|
|
#include <kexidb/transaction.h>
|
|
#include <widget/kexicharencodingcombobox.h>
|
|
|
|
#include "kexicsvimportdialog.h"
|
|
#include "kexicsvwidgets.h"
|
|
|
|
#ifdef TQ_WS_WIN
|
|
#include <tderecentdirs.h>
|
|
#include <windows.h>
|
|
#endif
|
|
|
|
#if 0
|
|
#include <kspread_cell.h>
|
|
#include <kspread_doc.h>
|
|
#include <kspread_sheet.h>
|
|
#include <kspread_undo.h>
|
|
#include <kspread_view.h>
|
|
#endif
|
|
|
|
#define _IMPORT_ICON "table" /*todo: change to "file_import" or so*/
|
|
#define _TEXT_TYPE 0
|
|
#define _NUMBER_TYPE 1
|
|
#define _DATE_TYPE 2
|
|
#define _TIME_TYPE 3
|
|
#define _DATETIME_TYPE 4
|
|
#define _PK_FLAG 5
|
|
|
|
//extra:
|
|
#define _NO_TYPE_YET -1 //allows to accept a number of empty cells, before something non-empty
|
|
#define _FP_NUMBER_TYPE 255 //_NUMBER_TYPE variant
|
|
#define MAX_ROWS_TO_PREVIEW 100 //max 100 rows is reasonable
|
|
#define MAX_BYTES_TO_PREVIEW 10240 //max 10KB is reasonable
|
|
#define MAX_CHARS_TO_SCAN_WHILE_DETECTING_DELIMITER 4096
|
|
|
|
class KexiCSVImportDialogTable : public TQTable
|
|
{
|
|
public:
|
|
KexiCSVImportDialogTable( TQWidget * parent = 0, const char * name = 0 )
|
|
: TQTable(parent, name) {
|
|
f = font();
|
|
f.setBold(true);
|
|
}
|
|
virtual void paintCell( TQPainter * p, int row, int col, const TQRect & cr, bool selected, const TQColorGroup & cg ) {
|
|
if (row==0)
|
|
p->setFont(f);
|
|
else
|
|
p->setFont(font());
|
|
TQTable::paintCell(p, row, col, cr, selected, cg);
|
|
}
|
|
virtual void setColumnWidth( int col, int w ) {
|
|
//make columns a bit wider
|
|
TQTable::setColumnWidth( col, w + 16 );
|
|
}
|
|
TQFont f;
|
|
};
|
|
|
|
//! Helper used to temporary disable keyboard and mouse events
|
|
void installRecursiveEventFilter(TQObject *filter, TQObject *object)
|
|
{
|
|
object->installEventFilter(filter);
|
|
|
|
TQObjectList clo = object->childrenListObject();
|
|
|
|
if (clo.isEmpty())
|
|
return;
|
|
|
|
TQObjectList list = clo;
|
|
for(TQObject *obj = list.first(); obj; obj = list.next())
|
|
installRecursiveEventFilter(filter, obj);
|
|
}
|
|
|
|
KexiCSVImportDialog::KexiCSVImportDialog( Mode mode, KexiMainWindow* mainWin,
|
|
TQWidget * parent, const char * name
|
|
)
|
|
: KDialogBase(
|
|
KDialogBase::Plain,
|
|
i18n( "Import CSV Data File" )
|
|
//! @todo use "Paste CSV Data From Clipboard" caption for mode==Clipboard
|
|
,
|
|
(mode==File ? User1 : (ButtonCode)0) |Ok|Cancel,
|
|
Ok,
|
|
parent,
|
|
name ? name : "KexiCSVImportDialog",
|
|
true,
|
|
false,
|
|
KGuiItem( i18n("&Options"))
|
|
),
|
|
m_mainWin(mainWin),
|
|
m_cancelled( false ),
|
|
m_adjustRows( true ),
|
|
m_startline( 0 ),
|
|
m_textquote( TQString(KEXICSV_DEFAULT_FILE_TEXT_QUOTE)[0] ),
|
|
m_mode(mode),
|
|
m_prevSelectedCol(-1),
|
|
m_columnsAdjusted(false),
|
|
m_1stRowForFieldNamesDetected(false),
|
|
m_firstFillTableCall(true),
|
|
m_blockUserEvents(false),
|
|
m_primaryKeyColumn(-1),
|
|
m_dialogCancelled(false),
|
|
m_conn(0),
|
|
m_destinationTableSchema(0),
|
|
m_allRowsLoadedInPreview(false),
|
|
m_stoppedAt_MAX_BYTES_TO_PREVIEW(false)
|
|
{
|
|
setWFlags(getWFlags() | TQt::WStyle_Maximize | TQt::WStyle_SysMenu);
|
|
hide();
|
|
setButtonOK(KGuiItem( i18n("&Import..."), _IMPORT_ICON));
|
|
|
|
m_typeNames.resize(5);
|
|
m_typeNames[0] = i18n("text");
|
|
m_typeNames[1] = i18n("number");
|
|
m_typeNames[2] = i18n("date");
|
|
m_typeNames[3] = i18n("time");
|
|
m_typeNames[4] = i18n("date/time");
|
|
|
|
kapp->config()->setGroup("ImportExport");
|
|
m_maximumRowsForPreview = kapp->config()->readNumEntry("MaximumRowsForPreviewInImportDialog", MAX_ROWS_TO_PREVIEW);
|
|
m_maximumBytesForPreview = kapp->config()->readNumEntry("MaximumBytesForPreviewInImportDialog", MAX_BYTES_TO_PREVIEW);
|
|
|
|
m_pkIcon = SmallIcon("key");
|
|
|
|
m_uniquenessTest.setAutoDelete(true);
|
|
|
|
setIcon(DesktopIcon(_IMPORT_ICON));
|
|
setSizeGripEnabled( TRUE );
|
|
|
|
// m_encoding = TQString::fromLatin1(TDEGlobal::locale()->encoding());
|
|
// m_stripWhiteSpaceInTextValuesChecked = true;
|
|
m_file = 0;
|
|
m_inputStream = 0;
|
|
|
|
TQVBoxLayout *lyr = new TQVBoxLayout(plainPage(), 0, KDialogBase::spacingHint(), "lyr");
|
|
|
|
m_infoLbl = new KexiCSVInfoLabel(
|
|
m_mode==File ? i18n("Preview of data from file:")
|
|
: i18n("Preview of data from clipboard:"),
|
|
plainPage()
|
|
);
|
|
lyr->addWidget( m_infoLbl );
|
|
|
|
TQWidget* page = new TQFrame( plainPage(), "page" );
|
|
TQGridLayout *glyr= new TQGridLayout( page, 4, 5, 0, KDialogBase::spacingHint(), "glyr");
|
|
lyr->addWidget( page );
|
|
|
|
// Delimiter: comma, semicolon, tab, space, other
|
|
m_delimiterWidget = new KexiCSVDelimiterWidget(true /*lineEditOnBottom*/, page);
|
|
m_detectDelimiter = true;
|
|
glyr->addMultiCellWidget( m_delimiterWidget, 1, 2, 0, 0 );
|
|
|
|
TQLabel *delimiterLabel = new TQLabel(m_delimiterWidget, i18n("Delimiter:"), page);
|
|
delimiterLabel->setAlignment(TQt::AlignAuto | TQt::AlignBottom);
|
|
glyr->addMultiCellWidget( delimiterLabel, 0, 0, 0, 0 );
|
|
|
|
// Format: number, text, currency,
|
|
m_formatComboText = i18n( "Format for column %1:" );
|
|
m_formatCombo = new KComboBox(page, "m_formatCombo");
|
|
m_formatCombo->insertItem(i18n("Text"));
|
|
m_formatCombo->insertItem(i18n("Number"));
|
|
m_formatCombo->insertItem(i18n("Date"));
|
|
m_formatCombo->insertItem(i18n("Time"));
|
|
m_formatCombo->insertItem(i18n("Date/Time"));
|
|
glyr->addMultiCellWidget( m_formatCombo, 1, 1, 1, 1 );
|
|
|
|
m_formatLabel = new TQLabel(m_formatCombo, "", page);
|
|
m_formatLabel->setAlignment(TQt::AlignAuto | TQt::AlignBottom);
|
|
glyr->addWidget( m_formatLabel, 0, 1 );
|
|
|
|
m_primaryKeyField = new TQCheckBox( i18n( "Primary key" ), page, "m_primaryKeyField" );
|
|
glyr->addWidget( m_primaryKeyField, 2, 1 );
|
|
connect(m_primaryKeyField, TQ_SIGNAL(toggled(bool)), this, TQ_SLOT(slotPrimaryKeyFieldToggled(bool)));
|
|
|
|
m_comboQuote = new KexiCSVTextQuoteComboBox( page );
|
|
glyr->addWidget( m_comboQuote, 1, 2 );
|
|
|
|
TextLabel2 = new TQLabel( m_comboQuote, i18n( "Text quote:" ), page, "TextLabel2" );
|
|
TextLabel2->setSizePolicy( TQSizePolicy::Fixed, TQSizePolicy::Preferred );
|
|
TextLabel2->setAlignment(TQt::AlignAuto | TQt::AlignBottom);
|
|
glyr->addWidget( TextLabel2, 0, 2 );
|
|
|
|
m_startAtLineSpinBox = new KIntSpinBox( page, "m_startAtLineSpinBox" );
|
|
m_startAtLineSpinBox->setMinValue(1);
|
|
m_startAtLineSpinBox->setSizePolicy( TQSizePolicy::Fixed, TQSizePolicy::Fixed );
|
|
m_startAtLineSpinBox->setMinimumWidth(TQFontMetrics(m_startAtLineSpinBox->font()).width("8888888"));
|
|
glyr->addWidget( m_startAtLineSpinBox, 1, 3 );
|
|
|
|
m_startAtLineLabel = new TQLabel( m_startAtLineSpinBox, "",
|
|
page, "TextLabel3" );
|
|
m_startAtLineLabel->setSizePolicy( TQSizePolicy::Fixed, TQSizePolicy::Preferred );
|
|
m_startAtLineLabel->setAlignment(TQt::AlignAuto | TQt::AlignBottom);
|
|
glyr->addWidget( m_startAtLineLabel, 0, 3 );
|
|
|
|
TQSpacerItem* spacer_2 = new TQSpacerItem( 0, 0, TQSizePolicy::Minimum, TQSizePolicy::Preferred );
|
|
glyr->addItem( spacer_2, 0, 4 );
|
|
|
|
m_ignoreDuplicates = new TQCheckBox( page, "m_ignoreDuplicates" );
|
|
m_ignoreDuplicates->setText( i18n( "Ignore duplicated delimiters" ) );
|
|
glyr->addMultiCellWidget( m_ignoreDuplicates, 2, 2, 2, 4 );
|
|
|
|
m_1stRowForFieldNames = new TQCheckBox( page, "m_1stRowForFieldNames" );
|
|
m_1stRowForFieldNames->setText( i18n( "First row contains column names" ) );
|
|
glyr->addMultiCellWidget( m_1stRowForFieldNames, 3, 3, 2, 4 );
|
|
|
|
m_table = new KexiCSVImportDialogTable( plainPage(), "m_table" );
|
|
lyr->addWidget( m_table );
|
|
|
|
m_table->setSizePolicy( TQSizePolicy(TQSizePolicy::MinimumExpanding, TQSizePolicy::MinimumExpanding, 1, 1) );
|
|
m_table->setNumRows( 0 );
|
|
m_table->setNumCols( 0 );
|
|
|
|
/** @todo reuse Clipboard too! */
|
|
/*
|
|
if ( m_mode == Clipboard )
|
|
{
|
|
setCaption( i18n( "Inserting From Clipboard" ) );
|
|
TQMimeSource * mime = TQApplication::clipboard()->data();
|
|
if ( !mime )
|
|
{
|
|
KMessageBox::information( this, i18n("There is no data in the clipboard.") );
|
|
m_cancelled = true;
|
|
return;
|
|
}
|
|
|
|
if ( !mime->provides( "text/plain" ) )
|
|
{
|
|
KMessageBox::information( this, i18n("There is no usable data in the clipboard.") );
|
|
m_cancelled = true;
|
|
return;
|
|
}
|
|
m_fileArray = TQByteArray(mime->encodedData( "text/plain" ) );
|
|
}
|
|
else if ( mode == File )
|
|
{*/
|
|
m_dateRegExp = TQRegExp("(\\d{1,4})([/\\-\\.])(\\d{1,2})([/\\-\\.])(\\d{1,4})");
|
|
m_timeRegExp1 = TQRegExp("(\\d{1,2}):(\\d{1,2}):(\\d{1,2})");
|
|
m_timeRegExp2 = TQRegExp("(\\d{1,2}):(\\d{1,2})");
|
|
m_fpNumberRegExp = TQRegExp("[\\-]{0,1}\\d*[,\\.]\\d+");
|
|
TQString caption( i18n("Open CSV Data File") );
|
|
|
|
if (m_mode == File) {
|
|
TQStringList mimetypes( csvMimeTypes() );
|
|
#ifdef TQ_WS_WIN
|
|
//! @todo remove
|
|
TQString recentDir = TDEGlobalSettings::documentPath();
|
|
m_fname = TQFileDialog::getOpenFileName(
|
|
KFileDialog::getStartURL(":CSVImportExport", recentDir).path(),
|
|
KexiUtils::fileDialogFilterStrings(mimetypes, false),
|
|
page, "KexiCSVImportDialog", caption);
|
|
if ( !m_fname.isEmpty() ) {
|
|
//save last visited path
|
|
KURL url;
|
|
url.setPath( m_fname );
|
|
if (url.isLocalFile())
|
|
TDERecentDirs::add(":CSVImportExport", url.directory());
|
|
}
|
|
#else
|
|
m_fname = KFileDialog::getOpenFileName(":CSVImportExport", mimetypes.join(" "),
|
|
this, caption);
|
|
#endif
|
|
//cancel action !
|
|
if ( m_fname.isEmpty() )
|
|
{
|
|
actionButton( Ok )->setEnabled( false );
|
|
m_cancelled = true;
|
|
if (parentWidget())
|
|
parentWidget()->raise();
|
|
return;
|
|
}
|
|
}
|
|
else if (m_mode == Clipboard) {
|
|
TQCString subtype("plain");
|
|
m_clipboardData = TQApplication::clipboard()->text(subtype, TQClipboard::Clipboard);
|
|
/* debug
|
|
for (int i=0;TQApplication::clipboard()->data(TQClipboard::Clipboard)->format(i);i++)
|
|
kdDebug() << i << ": "
|
|
<< TQApplication::clipboard()->data(TQClipboard::Clipboard)->format(i) << endl;
|
|
*/
|
|
}
|
|
else {
|
|
return;
|
|
}
|
|
|
|
m_loadingProgressDlg = 0;
|
|
m_importingProgressDlg = 0;
|
|
if (m_mode == File) {
|
|
m_loadingProgressDlg = new KProgressDialog(
|
|
this, "m_loadingProgressDlg", i18n("Loading CSV Data"), i18n("Loading CSV Data from \"%1\"...")
|
|
.arg(TQDir::convertSeparators(m_fname)), true);
|
|
m_loadingProgressDlg->progressBar()->setTotalSteps( m_maximumRowsForPreview+1 );
|
|
m_loadingProgressDlg->show();
|
|
}
|
|
|
|
if (m_mode==Clipboard) {
|
|
m_infoLbl->setIcon("edit-paste");
|
|
}
|
|
//updateRowCountInfo();
|
|
|
|
m_table->setSelectionMode(TQTable::NoSelection);
|
|
|
|
connect(m_formatCombo, TQ_SIGNAL(activated(int)),
|
|
this, TQ_SLOT(formatChanged(int)));
|
|
connect(m_delimiterWidget, TQ_SIGNAL(delimiterChanged(const TQString&)),
|
|
this, TQ_SLOT(delimiterChanged(const TQString&)));
|
|
connect(m_startAtLineSpinBox, TQ_SIGNAL(valueChanged ( int )),
|
|
this, TQ_SLOT(startlineSelected(int)));
|
|
connect(m_comboQuote, TQ_SIGNAL(activated(int)),
|
|
this, TQ_SLOT(textquoteSelected(int)));
|
|
connect(m_table, TQ_SIGNAL(currentChanged(int, int)),
|
|
this, TQ_SLOT(currentCellChanged(int, int)));
|
|
connect(m_table, TQ_SIGNAL(valueChanged(int,int)),
|
|
this, TQ_SLOT(cellValueChanged(int,int)));
|
|
connect(m_ignoreDuplicates, TQ_SIGNAL(stateChanged(int)),
|
|
this, TQ_SLOT(ignoreDuplicatesChanged(int)));
|
|
connect(m_1stRowForFieldNames, TQ_SIGNAL(stateChanged(int)),
|
|
this, TQ_SLOT(slot1stRowForFieldNamesChanged(int)));
|
|
|
|
connect(this, TQ_SIGNAL(user1Clicked()), this, TQ_SLOT(optionsButtonClicked()));
|
|
|
|
installRecursiveEventFilter(this, this);
|
|
|
|
initLater();
|
|
}
|
|
|
|
KexiCSVImportDialog::~KexiCSVImportDialog()
|
|
{
|
|
delete m_file;
|
|
}
|
|
|
|
void KexiCSVImportDialog::initLater()
|
|
{
|
|
if (!openData())
|
|
return;
|
|
|
|
// delimiterChanged(detectedDelimiter); // this will cause fillTable()
|
|
m_columnsAdjusted = false;
|
|
fillTable();
|
|
delete m_loadingProgressDlg;
|
|
m_loadingProgressDlg = 0;
|
|
if (m_dialogCancelled) {
|
|
// m_loadingProgressDlg->hide();
|
|
// m_loadingProgressDlg->close();
|
|
TQTimer::singleShot(0, this, TQ_SLOT(reject()));
|
|
return;
|
|
}
|
|
|
|
currentCellChanged(0, 0);
|
|
|
|
// updateGeometry();
|
|
adjustSize();
|
|
KDialog::centerOnScreen( this );
|
|
|
|
if (m_loadingProgressDlg)
|
|
m_loadingProgressDlg->hide();
|
|
show();
|
|
m_table->setFocus();
|
|
}
|
|
|
|
bool KexiCSVImportDialog::openData()
|
|
{
|
|
if (m_mode!=File) //data already loaded, no encoding stuff needed
|
|
return true;
|
|
|
|
delete m_inputStream;
|
|
m_inputStream = 0;
|
|
if (m_file) {
|
|
m_file->close();
|
|
delete m_file;
|
|
}
|
|
m_file = new TQFile(m_fname);
|
|
if (!m_file->open(IO_ReadOnly))
|
|
{
|
|
m_file->close();
|
|
delete m_file;
|
|
m_file = 0;
|
|
KMessageBox::sorry( this, i18n("Cannot open input file <nobr>\"%1\"</nobr>.")
|
|
.arg(TQDir::convertSeparators(m_fname)) );
|
|
actionButton( Ok )->setEnabled( false );
|
|
m_cancelled = true;
|
|
if (parentWidget())
|
|
parentWidget()->raise();
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool KexiCSVImportDialog::cancelled() const
|
|
{
|
|
return m_cancelled;
|
|
}
|
|
|
|
void KexiCSVImportDialog::fillTable()
|
|
{
|
|
KexiUtils::WaitCursor wc(true);
|
|
repaint();
|
|
m_blockUserEvents = true;
|
|
TQPushButton *pb = actionButton(KDialogBase::Cancel);
|
|
if (pb)
|
|
pb->setEnabled(true); //allow to cancel
|
|
KexiUtils::WaitCursor wait;
|
|
|
|
if (m_table->numRows()>0) //to accept editor
|
|
m_table->setCurrentCell(0,0);
|
|
|
|
int row, column, maxColumn;
|
|
TQString field = TQString();
|
|
|
|
for (row = 0; row < m_table->numRows(); ++row)
|
|
for (column = 0; column < m_table->numCols(); ++column)
|
|
m_table->clearCell(row, column);
|
|
|
|
m_detectedTypes.clear();
|
|
m_detectedTypes.resize(1024, _NO_TYPE_YET);//_TEXT_TYPE);
|
|
m_uniquenessTest.clear();
|
|
m_uniquenessTest.resize(1024);
|
|
m_1stRowForFieldNamesDetected = true;
|
|
|
|
if (true != loadRows(field, row, column, maxColumn, true))
|
|
return;
|
|
|
|
m_1stRowForFieldNamesDetected = false;
|
|
|
|
// file with only one line without '\n'
|
|
if (field.length() > 0)
|
|
{
|
|
setText(row - m_startline, column, field, true);
|
|
++row;
|
|
field = TQString();
|
|
}
|
|
|
|
adjustRows( row - m_startline - (m_1stRowForFieldNames->isChecked()?1:0) );
|
|
|
|
maxColumn = TQMAX( maxColumn, column );
|
|
m_table->setNumCols(maxColumn);
|
|
|
|
for (column = 0; column < m_table->numCols(); ++column)
|
|
{
|
|
// TQString header = m_table->horizontalHeader()->label(column);
|
|
// if (header != i18n("Text") && header != i18n("Number") &&
|
|
// header != i18n("Date") && header != i18n("Currency"))
|
|
// const int detectedType = m_detectedTypes[column+1];
|
|
// m_table->horizontalHeader()->setLabel(column, m_typeNames[ detectedType ]); //i18n("Text"));
|
|
updateColumnText(column);
|
|
if (!m_columnsAdjusted)
|
|
m_table->adjustColumn(column);
|
|
}
|
|
m_columnsAdjusted = true;
|
|
|
|
if (m_primaryKeyColumn>=0 && m_primaryKeyColumn<m_table->numCols()) {
|
|
if (_NUMBER_TYPE != m_detectedTypes[ m_primaryKeyColumn ]) {
|
|
m_primaryKeyColumn = -1;
|
|
}
|
|
}
|
|
|
|
m_prevSelectedCol = -1;
|
|
m_table->setCurrentCell(0,0);
|
|
currentCellChanged(0, 0);
|
|
if (m_primaryKeyColumn != -1)
|
|
m_table->setPixmap(0, m_primaryKeyColumn, m_pkIcon);
|
|
|
|
const int count = TQMAX(0, m_table->numRows()-1+m_startline);
|
|
m_allRowsLoadedInPreview = count < m_maximumRowsForPreview && !m_stoppedAt_MAX_BYTES_TO_PREVIEW;
|
|
if (m_allRowsLoadedInPreview) {
|
|
m_startAtLineSpinBox->setMaxValue(count);
|
|
m_startAtLineSpinBox->setValue(m_startline+1);
|
|
}
|
|
m_startAtLineLabel->setText(i18n( "Start at line%1:").arg(
|
|
m_allRowsLoadedInPreview ? TQString(" (1-%1)").arg(count)
|
|
: TQString() //we do not know what's real count
|
|
));
|
|
updateRowCountInfo();
|
|
|
|
m_blockUserEvents = false;
|
|
repaint();
|
|
m_table->verticalScrollBar()->repaint();//avoid missing repaint
|
|
m_table->horizontalScrollBar()->repaint();//avoid missing repaint
|
|
}
|
|
|
|
TQString KexiCSVImportDialog::detectDelimiterByLookingAtFirstBytesOfFile(TQTextStream& inputStream)
|
|
{
|
|
m_file->at(0);
|
|
|
|
// try to detect delimiter
|
|
// \t has priority, then ; then ,
|
|
const TQIODevice::Offset origOffset = inputStream.device()->at();
|
|
TQChar c, prevChar=0;
|
|
int detectedDelimiter = 0;
|
|
bool insideQuote = false;
|
|
|
|
//characters by priority
|
|
const int CH_TAB_AFTER_QUOTE = 500;
|
|
const int CH_SEMICOLON_AFTER_QUOTE = 499;
|
|
const int CH_COMMA_AFTER_QUOTE = 498;
|
|
const int CH_TAB = 200; // \t
|
|
const int CH_SEMICOLON = 199; // ;
|
|
const int CH_COMMA = 198; // ,
|
|
|
|
TQValueList<int> tabsPerLine, semicolonsPerLine, commasPerLine;
|
|
int tabs = 0, semicolons = 0, commas = 0;
|
|
int line = 0;
|
|
for (uint i=0; !inputStream.atEnd() && i < MAX_CHARS_TO_SCAN_WHILE_DETECTING_DELIMITER; i++) {
|
|
(*m_inputStream) >> c; // read one char
|
|
if (prevChar=='"') {
|
|
if (c!='"') //real quote (not double "")
|
|
insideQuote = !insideQuote;
|
|
}
|
|
if (insideQuote) {
|
|
prevChar = c;
|
|
continue;
|
|
}
|
|
if (c==' ')
|
|
continue;
|
|
if (c=='\n') {//end of line
|
|
//remember # of tabs/semicolons/commas in this line
|
|
tabsPerLine += tabs;
|
|
tabs = 0;
|
|
semicolonsPerLine += semicolons;
|
|
semicolons = 0;
|
|
commasPerLine += commas;
|
|
commas = 0;
|
|
line++;
|
|
}
|
|
else if (c=='\t') {
|
|
tabs++;
|
|
detectedDelimiter = TQMAX( prevChar=='"' ? CH_TAB_AFTER_QUOTE : CH_TAB, detectedDelimiter );
|
|
}
|
|
else if (c==';') {
|
|
semicolons++;
|
|
detectedDelimiter = TQMAX( prevChar=='"' ? CH_SEMICOLON_AFTER_QUOTE : CH_SEMICOLON, detectedDelimiter );
|
|
}
|
|
else if (c==',') {
|
|
commas++;
|
|
detectedDelimiter = TQMAX( prevChar=='"' ? CH_COMMA_AFTER_QUOTE : CH_COMMA, detectedDelimiter );
|
|
}
|
|
prevChar = c;
|
|
}
|
|
|
|
inputStream.device()->at(origOffset); //restore orig. offset
|
|
|
|
//now, try to find a delimiter character that exists the same number of times in all the checked lines
|
|
//this detection method has priority over others
|
|
TQValueList<int>::ConstIterator it;
|
|
if (tabsPerLine.count()>1) {
|
|
tabs = tabsPerLine.isEmpty() ? 0 : tabsPerLine.first();
|
|
for (it=tabsPerLine.constBegin(); it!=tabsPerLine.constEnd(); ++it) {
|
|
if (tabs != *it)
|
|
break;
|
|
}
|
|
if (tabs>0 && it==tabsPerLine.constEnd())
|
|
return "\t";
|
|
}
|
|
if (semicolonsPerLine.count()>1) {
|
|
semicolons = semicolonsPerLine.isEmpty() ? 0 : semicolonsPerLine.first();
|
|
for (it=semicolonsPerLine.constBegin(); it!=semicolonsPerLine.constEnd(); ++it) {
|
|
if (semicolons != *it)
|
|
break;
|
|
}
|
|
if (semicolons > 0 && it==semicolonsPerLine.constEnd())
|
|
return ";";
|
|
}
|
|
if (commasPerLine.count()>1) {
|
|
commas = commasPerLine.first();
|
|
for (it=commasPerLine.constBegin(); it!=commasPerLine.constEnd(); ++it) {
|
|
if (commas != *it)
|
|
break;
|
|
}
|
|
if (commas > 0 && it==commasPerLine.constEnd())
|
|
return ",";
|
|
}
|
|
//now return the winning character by looking at CH_* symbol
|
|
if (detectedDelimiter == CH_TAB_AFTER_QUOTE || detectedDelimiter == CH_TAB)
|
|
return "\t";
|
|
if (detectedDelimiter == CH_SEMICOLON_AFTER_QUOTE || detectedDelimiter == CH_SEMICOLON)
|
|
return ";";
|
|
if (detectedDelimiter == CH_COMMA_AFTER_QUOTE || detectedDelimiter == CH_COMMA)
|
|
return ",";
|
|
|
|
return KEXICSV_DEFAULT_FILE_DELIMITER; //<-- default
|
|
}
|
|
|
|
tristate KexiCSVImportDialog::loadRows(TQString &field, int &row, int &column, int &maxColumn,
|
|
bool inGUI)
|
|
{
|
|
enum { S_START, S_QUOTED_FIELD, S_MAYBE_END_OF_QUOTED_FIELD, S_END_OF_QUOTED_FIELD,
|
|
S_MAYBE_NORMAL_FIELD, S_NORMAL_FIELD } state = S_START;
|
|
field = TQString();
|
|
const bool ignoreDups = m_ignoreDuplicates->isChecked();
|
|
bool lastCharDelimiter = false;
|
|
bool nextRow = false;
|
|
row = column = 1;
|
|
maxColumn = 0;
|
|
TQChar x;
|
|
const bool hadInputStream = m_inputStream!=0;
|
|
delete m_inputStream;
|
|
if ( m_mode == Clipboard ) {
|
|
m_inputStream = new TQTextStream(m_clipboardData, IO_ReadOnly);
|
|
if (!hadInputStream)
|
|
m_delimiterWidget->setDelimiter(KEXICSV_DEFAULT_CLIPBOARD_DELIMITER);
|
|
}
|
|
else {
|
|
m_file->at(0); //always seek at 0 because loadRows() is called many times
|
|
m_inputStream = new TQTextStream(m_file);
|
|
if (m_options.defaultEncodingExplicitySet) {
|
|
TQTextCodec *codec = TDEGlobal::charsets()->codecForName(m_options.encoding);
|
|
if (codec)
|
|
m_inputStream->setCodec(codec); //TQTextCodec::codecForName("CP1250"));
|
|
}
|
|
if (m_detectDelimiter) {
|
|
const TQString delimiter( detectDelimiterByLookingAtFirstBytesOfFile(*m_inputStream) );
|
|
if (m_delimiterWidget->delimiter() != delimiter)
|
|
m_delimiterWidget->setDelimiter( delimiter );
|
|
}
|
|
}
|
|
const TQChar delimiter(m_delimiterWidget->delimiter()[0]);
|
|
m_stoppedAt_MAX_BYTES_TO_PREVIEW = false;
|
|
int progressStep = 0;
|
|
if (m_importingProgressDlg)
|
|
progressStep = TQMAX( 1, m_importingProgressDlg->progressBar()->totalSteps()/200 );
|
|
int offset = 0;
|
|
for (;!m_inputStream->atEnd(); offset++)
|
|
{
|
|
//disabled: this breaks wide spreadsheets
|
|
// if (column >= m_maximumRowsForPreview)
|
|
// return true;
|
|
|
|
if (m_importingProgressDlg && ((offset % progressStep) < 5)) {
|
|
//update progr. bar dlg on final exporting
|
|
m_importingProgressDlg->progressBar()->setValue(offset);
|
|
tqApp->processEvents();
|
|
if (m_importingProgressDlg->wasCancelled()) {
|
|
delete m_importingProgressDlg;
|
|
m_importingProgressDlg = 0;
|
|
return ::cancelled;
|
|
}
|
|
}
|
|
|
|
(*m_inputStream) >> x; // read one char
|
|
|
|
if (x == '\r') {
|
|
continue; // eat '\r', to handle RFC-compliant files
|
|
}
|
|
if (offset==0 && x.unicode()==0xfeff) {
|
|
// Ignore BOM, the "Byte Order Mark"
|
|
// (http://en.wikipedia.org/wiki/Byte_Order_Mark, // http://www.unicode.org/charts/PDF/UFFF0.pdf)
|
|
continue;
|
|
}
|
|
|
|
switch (state)
|
|
{
|
|
case S_START :
|
|
if (x == m_textquote)
|
|
{
|
|
state = S_QUOTED_FIELD;
|
|
}
|
|
else if (x == delimiter)
|
|
{
|
|
setText(row - m_startline, column, field, inGUI);
|
|
field = TQString();
|
|
if ((ignoreDups == false) || (lastCharDelimiter == false))
|
|
++column;
|
|
lastCharDelimiter = true;
|
|
}
|
|
else if (x == '\n')
|
|
{
|
|
if (!inGUI) {
|
|
//fill remaining empty fields (database wants them explicitly)
|
|
for (int additionalColumn = column; additionalColumn <= maxColumn; additionalColumn++) {
|
|
setText(row - m_startline, additionalColumn, TQString(), inGUI);
|
|
}
|
|
}
|
|
nextRow = true;
|
|
maxColumn = TQMAX( maxColumn, column );
|
|
column = 1;
|
|
}
|
|
else
|
|
{
|
|
field += x;
|
|
state = S_MAYBE_NORMAL_FIELD;
|
|
}
|
|
break;
|
|
case S_QUOTED_FIELD :
|
|
if (x == m_textquote)
|
|
{
|
|
state = S_MAYBE_END_OF_QUOTED_FIELD;
|
|
}
|
|
/*allow \n inside quoted fields
|
|
else if (x == '\n')
|
|
{
|
|
setText(row - m_startline, column, field, inGUI);
|
|
field = "";
|
|
if (x == '\n')
|
|
{
|
|
nextRow = true;
|
|
maxColumn = TQMAX( maxColumn, column );
|
|
column = 1;
|
|
}
|
|
else
|
|
{
|
|
if ((ignoreDups == false) || (lastCharDelimiter == false))
|
|
++column;
|
|
lastCharDelimiter = true;
|
|
}
|
|
state = S_START;
|
|
}*/
|
|
else
|
|
{
|
|
field += x;
|
|
}
|
|
break;
|
|
case S_MAYBE_END_OF_QUOTED_FIELD :
|
|
if (x == m_textquote)
|
|
{
|
|
field += x; //no, this was just escaped quote character
|
|
state = S_QUOTED_FIELD;
|
|
}
|
|
else if (x == delimiter || x == '\n')
|
|
{
|
|
setText(row - m_startline, column, field, inGUI);
|
|
field = TQString();
|
|
if (x == '\n')
|
|
{
|
|
nextRow = true;
|
|
maxColumn = TQMAX( maxColumn, column );
|
|
column = 1;
|
|
}
|
|
else
|
|
{
|
|
if ((ignoreDups == false) || (lastCharDelimiter == false))
|
|
++column;
|
|
lastCharDelimiter = true;
|
|
}
|
|
state = S_START;
|
|
}
|
|
else
|
|
{
|
|
state = S_END_OF_QUOTED_FIELD;
|
|
}
|
|
break;
|
|
case S_END_OF_QUOTED_FIELD :
|
|
if (x == delimiter || x == '\n')
|
|
{
|
|
setText(row - m_startline, column, field, inGUI);
|
|
field = TQString();
|
|
if (x == '\n')
|
|
{
|
|
nextRow = true;
|
|
maxColumn = TQMAX( maxColumn, column );
|
|
column = 1;
|
|
}
|
|
else
|
|
{
|
|
if ((ignoreDups == false) || (lastCharDelimiter == false))
|
|
++column;
|
|
lastCharDelimiter = true;
|
|
}
|
|
state = S_START;
|
|
}
|
|
else
|
|
{
|
|
state = S_END_OF_QUOTED_FIELD;
|
|
}
|
|
break;
|
|
case S_MAYBE_NORMAL_FIELD :
|
|
if (x == m_textquote)
|
|
{
|
|
field = TQString();
|
|
state = S_QUOTED_FIELD;
|
|
break;
|
|
}
|
|
case S_NORMAL_FIELD :
|
|
if (x == delimiter || x == '\n')
|
|
{
|
|
setText(row - m_startline, column, field, inGUI);
|
|
field = TQString();
|
|
if (x == '\n')
|
|
{
|
|
nextRow = true;
|
|
maxColumn = TQMAX( maxColumn, column );
|
|
column = 1;
|
|
}
|
|
else
|
|
{
|
|
if ((ignoreDups == false) || (lastCharDelimiter == false))
|
|
++column;
|
|
lastCharDelimiter = true;
|
|
}
|
|
state = S_START;
|
|
}
|
|
else
|
|
{
|
|
field += x;
|
|
}
|
|
}
|
|
if (x != delimiter)
|
|
lastCharDelimiter = false;
|
|
|
|
if (nextRow) {
|
|
if (!inGUI && row==1 && m_1stRowForFieldNames->isChecked()) {
|
|
// do not save to the database 1st row if it contains column names
|
|
m_importingStatement->clearArguments();
|
|
}
|
|
else if (!saveRow(inGUI))
|
|
return false;
|
|
|
|
++row;
|
|
}
|
|
|
|
if (m_firstFillTableCall && row==2
|
|
&& !m_1stRowForFieldNames->isChecked() && m_1stRowForFieldNamesDetected)
|
|
{
|
|
//'1st row for field name' flag detected: reload table
|
|
m_1stRowForFieldNamesDetected = false;
|
|
m_table->setNumRows( 0 );
|
|
m_firstFillTableCall = false; //this trick is allowed only once, on startup
|
|
m_1stRowForFieldNames->setChecked(true); //this will reload table
|
|
//slot1stRowForFieldNamesChanged(1);
|
|
m_blockUserEvents = false;
|
|
repaint();
|
|
return false;
|
|
}
|
|
|
|
if (!m_importingProgressDlg && row % 20 == 0) {
|
|
tqApp->processEvents();
|
|
//only for GUI mode:
|
|
if (!m_firstFillTableCall && m_loadingProgressDlg && m_loadingProgressDlg->wasCancelled()) {
|
|
delete m_loadingProgressDlg;
|
|
m_loadingProgressDlg = 0;
|
|
m_dialogCancelled = true;
|
|
reject();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (!m_firstFillTableCall && m_loadingProgressDlg) {
|
|
m_loadingProgressDlg->progressBar()->setValue(TQMIN(m_maximumRowsForPreview, row));
|
|
}
|
|
|
|
if ( inGUI && row > (m_maximumRowsForPreview + (m_1stRowForFieldNamesDetected?1:0)) ) {
|
|
kexipluginsdbg << "KexiCSVImportDialog::fillTable() loading stopped at row #"
|
|
<< m_maximumRowsForPreview << endl;
|
|
break;
|
|
}
|
|
if (nextRow) {
|
|
nextRow = false;
|
|
//additional speedup: stop processing now if too many bytes were loaded for preview
|
|
kexipluginsdbg << offset << endl;
|
|
if (inGUI && offset >= m_maximumBytesForPreview && row >= 2) {
|
|
m_stoppedAt_MAX_BYTES_TO_PREVIEW = true;
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void KexiCSVImportDialog::updateColumnText(int col)
|
|
{
|
|
TQString colName;
|
|
if (col<(int)m_columnNames.count() && (m_1stRowForFieldNames->isChecked() || m_changedColumnNames[col]))
|
|
colName = m_columnNames[ col ];
|
|
if (colName.isEmpty()) {
|
|
colName = i18n("Column %1").arg(col+1); //will be changed to a valid identifier on import
|
|
m_changedColumnNames[ col ] = false;
|
|
}
|
|
int detectedType = m_detectedTypes[col];
|
|
if (detectedType==_FP_NUMBER_TYPE)
|
|
detectedType=_NUMBER_TYPE; //we're simplifying that for now
|
|
else if (detectedType==_NO_TYPE_YET) {
|
|
m_detectedTypes[col]=_TEXT_TYPE; //entirely empty column
|
|
detectedType=_TEXT_TYPE;
|
|
}
|
|
m_table->horizontalHeader()->setLabel(col,
|
|
i18n("Column %1").arg(col+1) + " \n(" + m_typeNames[ detectedType ] + ") ");
|
|
m_table->setText(0, col, colName);
|
|
m_table->horizontalHeader()->adjustHeaderSize();
|
|
|
|
//check uniqueness
|
|
TQValueList<int> *list = m_uniquenessTest[col];
|
|
if (m_primaryKeyColumn==-1 && list && !list->isEmpty()) {
|
|
qHeapSort(*list);
|
|
TQValueList<int>::ConstIterator it=list->constBegin();
|
|
int prevValue = *it;
|
|
++it;
|
|
for(; it!=list->constEnd() && prevValue!=(*it); ++it)
|
|
prevValue=(*it);
|
|
if (it!=list->constEnd()) {
|
|
//duplicates:
|
|
list->clear();
|
|
}
|
|
else {
|
|
//a candidate for PK (autodetected)!
|
|
if (-1==m_primaryKeyColumn) {
|
|
m_primaryKeyColumn=col;
|
|
}
|
|
}
|
|
}
|
|
if (list) //not needed now: conserve memory
|
|
list->clear();
|
|
}
|
|
|
|
void KexiCSVImportDialog::detectTypeAndUniqueness(int row, int col, const TQString& text)
|
|
{
|
|
int intValue;
|
|
const int type = m_detectedTypes[col];
|
|
if (row==1 || type!=_TEXT_TYPE) {
|
|
bool found = false;
|
|
if (text.isEmpty() && type==_NO_TYPE_YET)
|
|
found = true; //real type should be found later
|
|
//detect type because it's 1st row or all prev. rows were not text
|
|
//-FP number? (trying before "number" type is a must)
|
|
if (!found && (row==1 || type==_NUMBER_TYPE || type==_FP_NUMBER_TYPE || type==_NO_TYPE_YET)) {
|
|
bool ok = text.isEmpty() || m_fpNumberRegExp.exactMatch(text);
|
|
//if (!ok)
|
|
// text.toDouble(&ok);
|
|
if (ok && (row==1 || type==_NUMBER_TYPE || type==_FP_NUMBER_TYPE || type==_NO_TYPE_YET)) {
|
|
m_detectedTypes[col]=_FP_NUMBER_TYPE;
|
|
found = true; //yes
|
|
}
|
|
}
|
|
//-number?
|
|
if (!found && (row==1 || type==_NUMBER_TYPE || type==_NO_TYPE_YET)) {
|
|
bool ok = text.isEmpty();//empty values allowed
|
|
if (!ok)
|
|
intValue = text.toInt(&ok);
|
|
if (ok && (row==1 || type==_NO_TYPE_YET)) {
|
|
m_detectedTypes[col]=_NUMBER_TYPE;
|
|
found = true; //yes
|
|
}
|
|
}
|
|
//-date?
|
|
if (!found && (row==1 || type==_DATE_TYPE || type==_NO_TYPE_YET)) {
|
|
if ((row==1 || type==_NO_TYPE_YET)
|
|
&& (text.isEmpty() || m_dateRegExp.exactMatch(text)))
|
|
{
|
|
m_detectedTypes[col]=_DATE_TYPE;
|
|
found = true; //yes
|
|
}
|
|
}
|
|
//-time?
|
|
if (!found && (row==1 || type==_TIME_TYPE || type==_NO_TYPE_YET)) {
|
|
if ((row==1 || type==_NO_TYPE_YET)
|
|
&& (text.isEmpty() || m_timeRegExp1.exactMatch(text) || m_timeRegExp2.exactMatch(text)))
|
|
{
|
|
m_detectedTypes[col]=_TIME_TYPE;
|
|
found = true; //yes
|
|
}
|
|
}
|
|
//-date/time?
|
|
if (!found && (row==1 || type==_TIME_TYPE || type==_NO_TYPE_YET)) {
|
|
if (row==1 || type==_NO_TYPE_YET) {
|
|
bool detected = text.isEmpty();
|
|
if (!detected) {
|
|
const TQStringList dateTimeList( TQStringList::split(" ", text) );
|
|
bool ok = dateTimeList.count()>=2;
|
|
//! @todo also support ISODateTime's "T" separator?
|
|
//! @todo also support timezones?
|
|
if (ok) {
|
|
//try all combinations
|
|
TQString datePart( dateTimeList[0].stripWhiteSpace() );
|
|
TQString timePart( dateTimeList[1].stripWhiteSpace() );
|
|
ok = m_dateRegExp.exactMatch(datePart)
|
|
&& (m_timeRegExp1.exactMatch(timePart) || m_timeRegExp2.exactMatch(timePart));
|
|
}
|
|
detected = ok;
|
|
}
|
|
if (detected) {
|
|
m_detectedTypes[col]=_DATETIME_TYPE;
|
|
found = true; //yes
|
|
}
|
|
}
|
|
}
|
|
if (!found && type==_NO_TYPE_YET && !text.isEmpty()) {
|
|
//eventually, a non-emptytext after a while
|
|
m_detectedTypes[col]=_TEXT_TYPE;
|
|
found = true; //yes
|
|
}
|
|
//default: text type (already set)
|
|
}
|
|
//check uniqueness for this value
|
|
TQValueList<int> *list = m_uniquenessTest[col];
|
|
if (row==1 && (!list || !list->isEmpty()) && !text.isEmpty() && _NUMBER_TYPE == m_detectedTypes[col]) {
|
|
if (!list) {
|
|
list = new TQValueList<int>();
|
|
m_uniquenessTest.insert(col, list);
|
|
}
|
|
list->append( intValue );
|
|
}
|
|
else {
|
|
//the value is empty or uniqueness test failed in the past
|
|
if (list && !list->isEmpty())
|
|
list->clear(); //indicate that uniqueness test failed
|
|
}
|
|
}
|
|
|
|
bool KexiCSVImportDialog::parseDate(const TQString& text, TQDate& date)
|
|
{
|
|
if (!m_dateRegExp.exactMatch(text))
|
|
return false;
|
|
//dddd - dd - dddd
|
|
//1 2 3 4 5 <- pos
|
|
const int d1 = m_dateRegExp.cap(1).toInt(), d3 = m_dateRegExp.cap(3).toInt(), d5 = m_dateRegExp.cap(5).toInt();
|
|
if (m_dateRegExp.cap(2)=="/") //probably separator for american format mm/dd/yyyy
|
|
date = TQDate(d5, d1, d3);
|
|
else {
|
|
if (d5 > 31) //d5 == year
|
|
date = TQDate(d5, d3, d1);
|
|
else //d1 == year
|
|
date = TQDate(d1, d3, d5);
|
|
}
|
|
return date.isValid();
|
|
}
|
|
|
|
bool KexiCSVImportDialog::parseTime(const TQString& text, TQTime& time)
|
|
{
|
|
time = TQTime::fromString(text, TQt::ISODate); //same as m_timeRegExp1
|
|
if (time.isValid())
|
|
return true;
|
|
if (m_timeRegExp2.exactMatch(text)) { //hh:mm:ss
|
|
time = TQTime(m_timeRegExp2.cap(1).toInt(), m_timeRegExp2.cap(3).toInt(), m_timeRegExp2.cap(5).toInt());
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void KexiCSVImportDialog::setText(int row, int col, const TQString& text, bool inGUI)
|
|
{
|
|
if (!inGUI) {
|
|
//save text directly to database buffer
|
|
if (col==1) { //1st col
|
|
m_importingStatement->clearArguments();
|
|
if (m_implicitPrimaryKeyAdded)
|
|
*m_importingStatement << TQVariant(); //id will be autogenerated here
|
|
}
|
|
const int detectedType = m_detectedTypes[col-1];
|
|
if (detectedType==_NUMBER_TYPE) {
|
|
*m_importingStatement << ( text.isEmpty() ? TQVariant() : text.toInt() );
|
|
//! @todo what about time and float/double types and different integer subtypes?
|
|
}
|
|
else if (detectedType==_FP_NUMBER_TYPE) {
|
|
//replace ',' with '.'
|
|
TQCString t(text.latin1());
|
|
const int textLen = t.length();
|
|
for (int i=0; i<textLen; i++) {
|
|
if (t.at(i)==',') {
|
|
t.at(i) = '.';
|
|
break;
|
|
}
|
|
}
|
|
*m_importingStatement << ( t.isEmpty() ? TQVariant() : t.toDouble() );
|
|
}
|
|
else if (detectedType==_DATE_TYPE) {
|
|
TQDate date;
|
|
if (parseDate(text, date))
|
|
*m_importingStatement << date;
|
|
}
|
|
else if (detectedType==_TIME_TYPE) {
|
|
TQTime time;
|
|
if (parseTime(text, time))
|
|
*m_importingStatement << time;
|
|
}
|
|
else if (detectedType==_DATETIME_TYPE) {
|
|
TQStringList dateTimeList( TQStringList::split(" ", text) );
|
|
if (dateTimeList.count()<2)
|
|
dateTimeList = TQStringList::split("T", text); //also support ISODateTime's "T" separator
|
|
//! @todo also support timezones?
|
|
if (dateTimeList.count()>=2) {
|
|
TQString datePart( dateTimeList[0].stripWhiteSpace() );
|
|
TQDate date;
|
|
if (parseDate(datePart, date)) {
|
|
TQString timePart( dateTimeList[1].stripWhiteSpace() );
|
|
TQTime time;
|
|
if (parseTime(timePart, time))
|
|
*m_importingStatement << TQDateTime(date, time);
|
|
}
|
|
}
|
|
}
|
|
else //_TEXT_TYPE and the rest
|
|
*m_importingStatement << (m_options.stripWhiteSpaceInTextValuesChecked ? text.stripWhiteSpace() : text);
|
|
return;
|
|
}
|
|
//save text to GUI (table view)
|
|
if (m_table->numCols() < col) {
|
|
m_table->setNumCols(col);
|
|
if ((int)m_columnNames.size() < m_table->numCols()) {
|
|
m_columnNames.resize(m_table->numCols()+10);
|
|
m_changedColumnNames.resize(m_table->numCols()+10);
|
|
}
|
|
}
|
|
|
|
if (m_1stRowForFieldNames->isChecked()) {
|
|
if ((row+m_startline)==1) {//this is for column name
|
|
if ((col-1) < (int)m_changedColumnNames.size() && (int)false==(int)m_changedColumnNames[col-1]) {
|
|
//this column has no custom name entered by a user
|
|
//-get the name from the data cell
|
|
TQString colName(text.simplifyWhiteSpace());
|
|
if (!colName.isEmpty()) {
|
|
if (colName.left(1)>="0" && colName.left(1)<="9")
|
|
colName.prepend(i18n("Column")+" ");
|
|
m_columnNames[ col-1 ] = colName;
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
else {
|
|
if ((row+m_startline)==1) {//this row is for column name
|
|
if (m_1stRowForFieldNamesDetected && !m_1stRowForFieldNames->isChecked()) {
|
|
TQString f( text.simplifyWhiteSpace() );
|
|
if (f.isEmpty() || !f[0].isLetter())
|
|
m_1stRowForFieldNamesDetected = false; //this couldn't be a column name
|
|
}
|
|
}
|
|
row++; //1st row was for column names
|
|
}
|
|
|
|
if (row < 2) // skipped by the user
|
|
return;
|
|
|
|
if (m_table->numRows() < row) {
|
|
// if (m_maximumRowsForPreview >= row+100)
|
|
m_table->setNumRows(row+100); /* We add more rows at a time to limit recalculations */
|
|
//else
|
|
// m_table->setNumRows(m_maximumRowsForPreview);
|
|
m_table->verticalHeader()->setLabel(0, i18n("Column name")+" ");
|
|
m_adjustRows=true;
|
|
}
|
|
|
|
m_table->setText(row - 1, col - 1, (m_options.stripWhiteSpaceInTextValuesChecked ? text.stripWhiteSpace() : text));
|
|
m_table->verticalHeader()->setLabel(row-1, TQString::number(row-1));
|
|
|
|
detectTypeAndUniqueness(row-1, col-1, text);
|
|
}
|
|
|
|
bool KexiCSVImportDialog::saveRow(bool inGUI)
|
|
{
|
|
if (inGUI) {
|
|
//nothing to do
|
|
return true;
|
|
}
|
|
//save db buffer
|
|
bool res = m_importingStatement->execute();
|
|
//todo: move
|
|
m_importingStatement->clearArguments();
|
|
return res;
|
|
// return m_conn->insertRecord(*m_destinationTableSchema, m_dbRowBuffer);
|
|
}
|
|
|
|
void KexiCSVImportDialog::adjustRows(int iRows)
|
|
{
|
|
if (m_adjustRows)
|
|
{
|
|
m_table->setNumRows( iRows );
|
|
m_adjustRows=false;
|
|
for (int i = 0; i<iRows; i++)
|
|
m_table->adjustRow(i);
|
|
}
|
|
}
|
|
|
|
void KexiCSVImportDialog::formatChanged(int id)
|
|
{
|
|
if (id==_PK_FLAG) {
|
|
if (m_primaryKeyColumn>=0 && m_primaryKeyColumn<m_table->numCols()) {
|
|
m_table->setPixmap(0, m_primaryKeyColumn, TQPixmap());
|
|
}
|
|
if (m_primaryKeyField->isChecked()) {
|
|
m_primaryKeyColumn = m_table->currentColumn();
|
|
m_table->setPixmap(0, m_primaryKeyColumn, m_pkIcon);
|
|
}
|
|
else
|
|
m_primaryKeyColumn = -1;
|
|
return;
|
|
}
|
|
else {
|
|
m_detectedTypes[m_table->currentColumn()]=id;
|
|
m_primaryKeyField->setEnabled( _NUMBER_TYPE == id );
|
|
m_primaryKeyField->setChecked( m_primaryKeyColumn == m_table->currentColumn() && m_primaryKeyField->isEnabled() );
|
|
}
|
|
updateColumnText(m_table->currentColumn());
|
|
}
|
|
|
|
void KexiCSVImportDialog::delimiterChanged(const TQString& delimiter)
|
|
{
|
|
Q_UNUSED(delimiter);
|
|
m_columnsAdjusted = false;
|
|
m_detectDelimiter = false; //selected by hand: do not detect in the future
|
|
//delayed, otherwise combobox won't be repainted
|
|
fillTableLater();
|
|
}
|
|
|
|
void KexiCSVImportDialog::textquoteSelected(int)
|
|
{
|
|
const TQString tq(m_comboQuote->textQuote());
|
|
if (tq.isEmpty())
|
|
m_textquote = 0;
|
|
else
|
|
m_textquote = tq[0];
|
|
|
|
kexipluginsdbg << "KexiCSVImportDialog::textquoteSelected(): " << m_textquote << endl;
|
|
|
|
//delayed, otherwise combobox won't be repainted
|
|
fillTableLater();
|
|
}
|
|
|
|
void KexiCSVImportDialog::fillTableLater()
|
|
{
|
|
m_table->setNumRows( 0 );
|
|
TQTimer::singleShot(10, this, TQ_SLOT(fillTable()));
|
|
}
|
|
|
|
void KexiCSVImportDialog::startlineSelected(int startline)
|
|
{
|
|
// const int startline = line.toInt() - 1;
|
|
if (m_startline == (startline-1))
|
|
return;
|
|
m_startline = startline-1;
|
|
m_adjustRows=true;
|
|
fillTable();
|
|
m_table->setFocus();
|
|
}
|
|
|
|
void KexiCSVImportDialog::currentCellChanged(int, int col)
|
|
{
|
|
if (m_prevSelectedCol==col)
|
|
return;
|
|
m_prevSelectedCol = col;
|
|
int type = m_detectedTypes[col];
|
|
if (type==_FP_NUMBER_TYPE)
|
|
type=_NUMBER_TYPE; //we're simplifying that for now
|
|
|
|
m_formatCombo->setCurrentItem( type );
|
|
m_formatLabel->setText( m_formatComboText.arg(col+1) );
|
|
m_primaryKeyField->setEnabled( _NUMBER_TYPE == m_detectedTypes[col]);
|
|
m_primaryKeyField->blockSignals(true); //block to disable executing slotPrimaryKeyFieldToggled()
|
|
m_primaryKeyField->setChecked( m_primaryKeyColumn == col );
|
|
m_primaryKeyField->blockSignals(false);
|
|
}
|
|
|
|
void KexiCSVImportDialog::cellValueChanged(int row,int col)
|
|
{
|
|
if (row==0) {//column name has changed
|
|
m_columnNames[ col ] = m_table->text(row, col);
|
|
m_changedColumnNames.setBit( col );
|
|
}
|
|
}
|
|
|
|
void KexiCSVImportDialog::accept()
|
|
{
|
|
//! @todo MOVE MOST OF THIS TO CORE/ (KexiProject?) after KexiDialogBase code is moved to non-gui place
|
|
|
|
KexiGUIMessageHandler msg; //! @todo make it better integrated with main window
|
|
|
|
const uint numRows( m_table->numRows() );
|
|
if (numRows == 0)
|
|
return; //impossible
|
|
|
|
if (numRows == 1) {
|
|
if (KMessageBox::No == KMessageBox::questionYesNo(this,
|
|
i18n("Data set contains no rows. Do you want to import empty table?")))
|
|
return;
|
|
}
|
|
|
|
KexiProject* project = m_mainWin->project();
|
|
if (!project) {
|
|
msg.showErrorMessage(i18n("No project available."));
|
|
return;
|
|
}
|
|
m_conn = project->dbConnection(); //cache this pointer
|
|
if (!m_conn) {
|
|
msg.showErrorMessage(i18n("No database connection available."));
|
|
return;
|
|
}
|
|
KexiPart::Part *part = Kexi::partManager().partForMimeType("kexi/table");
|
|
if (!part) {
|
|
msg.showErrorMessage(&Kexi::partManager());
|
|
return;
|
|
}
|
|
|
|
//get suggested name based on the file name
|
|
TQString suggestedName;
|
|
if (m_mode==File) {
|
|
suggestedName = KURL::fromPathOrURL(m_fname).fileName();
|
|
//remove extension
|
|
if (!suggestedName.isEmpty()) {
|
|
const int idx = suggestedName.findRev(".");
|
|
if (idx!=-1)
|
|
suggestedName = suggestedName.mid(0, idx ).simplifyWhiteSpace();
|
|
}
|
|
}
|
|
|
|
//-new part item
|
|
KexiPart::Item* partItemForSavedTable = project->createPartItem(part->info(), suggestedName);
|
|
if (!partItemForSavedTable) {
|
|
// msg.showErrorMessage(project);
|
|
return;
|
|
}
|
|
|
|
#define _ERR \
|
|
{ project->deleteUnstoredItem(partItemForSavedTable); \
|
|
m_conn = 0; \
|
|
delete m_destinationTableSchema; \
|
|
m_destinationTableSchema = 0; \
|
|
return; }
|
|
|
|
//-ask for table name/title
|
|
// (THIS IS FROM KexiMainWindowImpl::saveObject())
|
|
bool allowOverwriting = true;
|
|
tristate res = m_mainWin->getNewObjectInfo( partItemForSavedTable, part, allowOverwriting );
|
|
if (~res || !res) {
|
|
//! @todo: err
|
|
_ERR;
|
|
}
|
|
//(allowOverwriting is now set to true, if user accepts overwriting,
|
|
// and overwriting will be needed)
|
|
|
|
// KexiDB::SchemaData sdata(part->info()->projectPartID());
|
|
// sdata.setName( partItem->name() );
|
|
|
|
//-create table schema (and thus schema object)
|
|
//-assign information (THIS IS FROM KexiDialogBase::storeNewData())
|
|
m_destinationTableSchema = new KexiDB::TableSchema(partItemForSavedTable->name());
|
|
m_destinationTableSchema->setCaption( partItemForSavedTable->caption() );
|
|
m_destinationTableSchema->setDescription( partItemForSavedTable->description() );
|
|
const uint numCols( m_table->numCols() );
|
|
|
|
m_implicitPrimaryKeyAdded = false;
|
|
//add PK if user wanted it
|
|
int msgboxResult;
|
|
if (m_primaryKeyColumn==-1
|
|
&& KMessageBox::No != (msgboxResult = KMessageBox::questionYesNoCancel(this,
|
|
i18n("No Primary Key (autonumber) has been defined.\n"
|
|
"Should it be automatically defined on import (recommended)?\n\n"
|
|
"Note: An imported table without a Primary Key may not be editable (depending on database type)."),
|
|
TQString(), KGuiItem(i18n("Add Database Primary Key to a Table", "Add Primary Key"), "key"),
|
|
KGuiItem(i18n("Do Not Add Database Primary Key to a Table", "Do Not Add")))))
|
|
{
|
|
if (msgboxResult == KMessageBox::Cancel)
|
|
_ERR; //cancel accepting
|
|
|
|
//add implicit PK field
|
|
//! @todo make this field hidden (what about e.g. pgsql?)
|
|
m_implicitPrimaryKeyAdded = true;
|
|
|
|
TQString fieldName("id");
|
|
TQString fieldCaption("Id");
|
|
|
|
TQStringList colnames;
|
|
for (uint col = 0; col < numCols; col++)
|
|
colnames.append( m_table->text(0, col).lower().simplifyWhiteSpace() );
|
|
|
|
if (colnames.find(fieldName)!=colnames.end()) {
|
|
int num = 1;
|
|
while (colnames.find(fieldName+TQString::number(num))!=colnames.end())
|
|
num++;
|
|
fieldName += TQString::number(num);
|
|
fieldCaption += TQString::number(num);
|
|
}
|
|
KexiDB::Field *field = new KexiDB::Field(
|
|
fieldName,
|
|
KexiDB::Field::Integer,
|
|
KexiDB::Field::NoConstraints,
|
|
KexiDB::Field::NoOptions,
|
|
0,0, //uint length=0, uint precision=0,
|
|
TQVariant(), //TQVariant defaultValue=TQVariant(),
|
|
fieldCaption
|
|
); //no description and width for now
|
|
field->setPrimaryKey(true);
|
|
field->setAutoIncrement(true);
|
|
m_destinationTableSchema->addField( field );
|
|
}
|
|
|
|
for (uint col = 0; col < numCols; col++) {
|
|
TQString fieldCaption( m_table->text(0, col).simplifyWhiteSpace() );
|
|
TQString fieldName( KexiUtils::string2Identifier( fieldCaption ) );
|
|
if (m_destinationTableSchema->field(fieldName)) {
|
|
TQString fixedFieldName;
|
|
uint i = 2; //"apple 2, apple 3, etc. if there're many "apple" names
|
|
do {
|
|
fixedFieldName = fieldName + "_" + TQString::number(i);
|
|
if (!m_destinationTableSchema->field(fixedFieldName))
|
|
break;
|
|
i++;
|
|
} while (true);
|
|
fieldName = fixedFieldName;
|
|
fieldCaption += (" " + TQString::number(i));
|
|
}
|
|
const int detectedType = m_detectedTypes[col];
|
|
KexiDB::Field::Type fieldType;
|
|
if (detectedType==_DATE_TYPE)
|
|
fieldType = KexiDB::Field::Date;
|
|
if (detectedType==_TIME_TYPE)
|
|
fieldType = KexiDB::Field::Time;
|
|
if (detectedType==_DATETIME_TYPE)
|
|
fieldType = KexiDB::Field::DateTime;
|
|
else if (detectedType==_NUMBER_TYPE)
|
|
fieldType = KexiDB::Field::Integer;
|
|
else if (detectedType==_FP_NUMBER_TYPE)
|
|
fieldType = KexiDB::Field::Double;
|
|
//! @todo what about time and float/double types and different integer subtypes?
|
|
else //_TEXT_TYPE and the rest
|
|
fieldType = KexiDB::Field::Text;
|
|
//! @todo what about long text?
|
|
|
|
KexiDB::Field *field = new KexiDB::Field(
|
|
fieldName,
|
|
fieldType,
|
|
KexiDB::Field::NoConstraints,
|
|
KexiDB::Field::NoOptions,
|
|
0,0, //uint length=0, uint precision=0,
|
|
TQVariant(), //TQVariant defaultValue=TQVariant(),
|
|
fieldCaption
|
|
); //no description and width for now
|
|
|
|
if ((int)col == m_primaryKeyColumn) {
|
|
field->setPrimaryKey(true);
|
|
field->setAutoIncrement(true);
|
|
}
|
|
m_destinationTableSchema->addField( field );
|
|
}
|
|
|
|
KexiDB::Transaction transaction = m_conn->beginTransaction();
|
|
if (transaction.isNull()) {
|
|
msg.showErrorMessage(m_conn);
|
|
_ERR;
|
|
}
|
|
KexiDB::TransactionGuard tg(transaction);
|
|
|
|
//-create physical table
|
|
if (!m_conn->createTable(m_destinationTableSchema, allowOverwriting)) {
|
|
msg.showErrorMessage(m_conn);
|
|
_ERR;
|
|
}
|
|
|
|
#define _DROP_DEST_TABLE_AND_RETURN \
|
|
{ \
|
|
if (m_importingProgressDlg) \
|
|
m_importingProgressDlg->hide(); \
|
|
project->deleteUnstoredItem(partItemForSavedTable); \
|
|
m_conn->dropTable(m_destinationTableSchema); /*alsoRemoveSchema*/ \
|
|
m_destinationTableSchema = 0; \
|
|
m_conn = 0; \
|
|
return; \
|
|
}
|
|
|
|
m_importingStatement = m_conn->prepareStatement(
|
|
KexiDB::PreparedStatement::InsertStatement, *m_destinationTableSchema);
|
|
if (!m_importingStatement) {
|
|
msg.showErrorMessage(m_conn);
|
|
_DROP_DEST_TABLE_AND_RETURN;
|
|
}
|
|
|
|
if (m_file) {
|
|
if (!m_importingProgressDlg) {
|
|
m_importingProgressDlg = new KProgressDialog( this, "m_importingProgressDlg",
|
|
i18n("Importing CSV Data"), TQString(), true );
|
|
}
|
|
m_importingProgressDlg->setLabel(
|
|
i18n("Importing CSV Data from <nobr>\"%1\"</nobr> into \"%2\" table...")
|
|
.arg(TQDir::convertSeparators(m_fname)).arg(m_destinationTableSchema->name()) );
|
|
m_importingProgressDlg->progressBar()->setTotalSteps( TQFileInfo(*m_file).size() );
|
|
m_importingProgressDlg->show();
|
|
}
|
|
|
|
int row, column, maxColumn;
|
|
TQString field = TQString();
|
|
|
|
// main job
|
|
res = loadRows(field, row, column, maxColumn, false /*!gui*/ );
|
|
|
|
delete m_importingProgressDlg;
|
|
m_importingProgressDlg = 0;
|
|
if (true != res) {
|
|
//importing cancelled or failed
|
|
if (!res) //do not display err msg when res == cancelled
|
|
msg.showErrorMessage(m_conn);
|
|
_DROP_DEST_TABLE_AND_RETURN;
|
|
}
|
|
|
|
// file with only one line without '\n'
|
|
if (field.length() > 0)
|
|
{
|
|
setText(row - m_startline, column, field, false /*!gui*/);
|
|
//fill remaining empty fields (database wants them explicitly)
|
|
for (int additionalColumn = column; additionalColumn <= maxColumn; additionalColumn++) {
|
|
setText(row - m_startline, additionalColumn, TQString(), false /*!gui*/);
|
|
}
|
|
if (!saveRow(false /*!gui*/)) {
|
|
msg.showErrorMessage(m_conn);
|
|
_DROP_DEST_TABLE_AND_RETURN;
|
|
}
|
|
++row;
|
|
field = TQString();
|
|
}
|
|
|
|
if (!tg.commit()) {
|
|
msg.showErrorMessage(m_conn);
|
|
_DROP_DEST_TABLE_AND_RETURN;
|
|
}
|
|
|
|
//-now we can store the item
|
|
partItemForSavedTable->setIdentifier( m_destinationTableSchema->id() );
|
|
project->addStoredItem( part->info(), partItemForSavedTable );
|
|
|
|
TQDialog::accept();
|
|
KMessageBox::information(this, i18n("Data has been successfully imported to table \"%1\".")
|
|
.arg(m_destinationTableSchema->name()));
|
|
parentWidget()->raise();
|
|
m_conn = 0;
|
|
}
|
|
|
|
int KexiCSVImportDialog::getHeader(int col)
|
|
{
|
|
TQString header = m_table->horizontalHeader()->label(col);
|
|
|
|
if (header == i18n("Text type for column", "Text"))
|
|
return TEXT;
|
|
else if (header == i18n("Numeric type for column", "Number"))
|
|
return NUMBER;
|
|
else if (header == i18n("Currency type for column", "Currency"))
|
|
return CURRENCY;
|
|
else
|
|
return DATE;
|
|
}
|
|
|
|
TQString KexiCSVImportDialog::getText(int row, int col)
|
|
{
|
|
return m_table->text(row, col);
|
|
}
|
|
|
|
void KexiCSVImportDialog::ignoreDuplicatesChanged(int)
|
|
{
|
|
fillTable();
|
|
}
|
|
|
|
void KexiCSVImportDialog::slot1stRowForFieldNamesChanged(int)
|
|
{
|
|
m_adjustRows=true;
|
|
if (m_1stRowForFieldNames->isChecked() && m_startline>0 && m_startline>=(m_startAtLineSpinBox->maxValue()-1))
|
|
m_startline--;
|
|
fillTable();
|
|
}
|
|
|
|
void KexiCSVImportDialog::optionsButtonClicked()
|
|
{
|
|
KexiCSVImportOptionsDialog dlg(m_options, this);
|
|
if (TQDialog::Accepted != dlg.exec())
|
|
return;
|
|
|
|
KexiCSVImportOptions newOptions( dlg.options() );
|
|
if (m_options != newOptions) {
|
|
m_options = newOptions;
|
|
if (!openData())
|
|
return;
|
|
fillTable();
|
|
}
|
|
}
|
|
|
|
bool KexiCSVImportDialog::eventFilter ( TQObject * watched, TQEvent * e )
|
|
{
|
|
TQEvent::Type t = e->type();
|
|
// temporary disable keyboard and mouse events for time-consuming tasks
|
|
if (m_blockUserEvents && (t==TQEvent::KeyPress || t==TQEvent::KeyRelease
|
|
|| t==TQEvent::MouseButtonPress || t==TQEvent::MouseButtonDblClick
|
|
|| t==TQEvent::Paint ))
|
|
return true;
|
|
|
|
if (watched == m_startAtLineSpinBox && t==TQEvent::KeyPress) {
|
|
TQKeyEvent *ke = static_cast<TQKeyEvent*>(e);
|
|
if (ke->key()==TQt::Key_Enter || ke->key()==TQt::Key_Return) {
|
|
m_table->setFocus();
|
|
return true;
|
|
}
|
|
}
|
|
return TQDialog::eventFilter( watched, e );
|
|
}
|
|
|
|
void KexiCSVImportDialog::slotPrimaryKeyFieldToggled(bool on)
|
|
{
|
|
Q_UNUSED(on);
|
|
formatChanged(_PK_FLAG);
|
|
}
|
|
|
|
void KexiCSVImportDialog::updateRowCountInfo()
|
|
{
|
|
m_infoLbl->setFileName( m_fname );
|
|
if (m_allRowsLoadedInPreview) {
|
|
m_infoLbl->setCommentText(
|
|
i18n("row count", "(rows: %1)").arg( m_table->numRows()-1+m_startline ) );
|
|
TQToolTip::remove( m_infoLbl );
|
|
}
|
|
else {
|
|
m_infoLbl->setCommentText(
|
|
i18n("row count", "(rows: more than %1)").arg( m_table->numRows()-1+m_startline ) );
|
|
TQToolTip::add( m_infoLbl->commentLabel(), i18n("Not all rows are visible on this preview") );
|
|
}
|
|
}
|
|
|
|
#include "kexicsvimportdialog.moc"
|