diff --git a/src/doc/dev/settings.txt b/src/doc/dev/settings.txt index 1d3158a65..8922001c0 100644 --- a/src/doc/dev/settings.txt +++ b/src/doc/dev/settings.txt @@ -1,169 +1,177 @@ ----------------------------------------------------------- Settings stored in 'kexirc' config file This is official list. All other settings are unofficial and are subject to change. Started: 2004-08-20, js ----------------------------------------------------------- Group: MainWindow # percentage width of the Project Navigator pane -LeftDockPosition [integer: 0..100] # True if the Project Navigator pane is visible after startup. -ShowProjectNavigator [boolean] (default: true) # True if single click should open item in the Project Navigator. # Otherwise double click should be used to open item. # Meaningful when "single click to open files" option is set # in the Control Center. - SingleClickOpensItem [boolean] (default: true on Windows, system settings elsewhere) # Controls display of the global search box. # This option allows to disable the box if it crashes for reason unrelated to KEXI # (as in bug #390794). - GlobalSearchBoxEnabled [boolean] (default: true) Group: PropertyEditor # Font size in pixels. Obsolete since Kexi 3. See FontPointSize instead. -FontSize [integer] (default: system settings) # Font size in points. -FontPointSize [double] (default: system settings) Group: Notification Messages -AskBeforeDeleteRow [boolean] (default: true) TODO: -askBeforeOpeningFileReadOnly [boolean] (default: true) # If true, warning messages related to plugins, e.g. # "Errors encountered during loading plugins" # will be displayed on the application's startup. -ShowWarningsRelatedToPluginsLoading [boolean] (default: true) Group: General # especially useful for SQL-related messages TODO: -alwaysShowDetailsInMsgBoxes [boolean] (default: true for advanced Kexi mode) # True if internal debugger window should be displayed with Kexi. # The window shows database operations and enables extra actions like 'Show Form UI Code' # in the Form Designer. # Only available when KEXI_DEBUG_GUI build option is enabled. -ShowInternalDebugger [boolean] (default: false) +# True if two-digit year formats for dates are allowed, e.g. 5/20/18 (month/day/year as for en_US). +# If false, KEXI alters the formats by replacing two-digits with four-digits for year. +# The goal is accuracy, that is ensuring clarity for dates regardless of century, e.g. 2018 +# versus 1918. +# This setting affects input and display of date and date/time types. +# Since 3.2.0 +-AllowTwoDigitYearFormats [boolean] (default: false) + Group: File Dialogs # If the KEXI_USE_KFILEWIDGET build option is on and UseKFileWidget is true, # KF5's KFileWidget-based widget is used in places where embedded file widget is needed. # If the KEXI_USE_KFILEWIDGET build option is on and UseKFileWidget is false, # simple file requester widget is used in places where embedded file widget is needed. # To delete the override, delete the UseKFileWidget option in the aplication's config file. -UseKFileWidget [boolean] (default: not present) Group: Recent Dirs # A list of recently displayed directories in file dialogs related to images (e.g. images within forms). # See KexiImageBox::slotInsertFromFile() and slotSaveAs(); TODO: -LastVisitedImagePath [URL list] (default: empty) Group: TableView TODO: -add default values for KexiTableView::Appearance Group: TableDesigner TODO: -autogeneratePrimaryKeysOnTableDesignSaving [boolean] TODO: -defaultFieldType [the list of types], default=Text TODO: -autoPrimaryKeyForFieldNames [stringlist] TODO: -defaultIntegerFieldSubtype [the list of types (byte, short, etc.)] default=long # Settings related to handling of database tables Group: Tables # value of DefaultTextFieldMaxLength should be also used on other places where we create tables with Text fields, e.g. on table importing # Default maximum length for fields of type Text TODO: -DefaultMaxLengthForTextFields [int] (0: unlimited or up to engine's limit, default=0) Group: QueryDesigner TODO: -autoJoinOnTableInserting [boolean] Group: KeyboardNavigation TODO: -cursorPlacementAfterLastOrFirstFormField [stringlist: nextOrPrevRecord|firstOrLastField(default)] Group: Forms TODO:-overrideStyleName [string] (empty if do not override) TODO:-doNotFocusAutonumberFields [boolean] (true by default; when this and autoTabStop for a form is true, autonumber text fields are skipped) implement this in KexiFormView::afterSwitchFrom() TODO:-appendColonToAutoLabels [boolean] (true by default; when true, colon character is appended to autolabel text) TODO:-makeFirstCharacterUpperCaseInAutoLabels [boolean] (true by default; when true, first character in autolabel text is converted to upper case. Usable when no field's title is provided) TODO:-labelPositionInAutoLabels [enum: Left, Top] (Left by default) TODO:-gridSize [int] (default: 10) Group: NewFormDefaults TODO: -styleName [string] TODO: -autoTabStop [boolean] Group: ImportExport # Default character encoding for MS Access MDB/MDE files (older than 2000). # Currently used by in Advanced Options of Importing Wizard. # Useful if you are performing many imports of MS Access databases. # Valid values can be "cp 1250", "cp 1251", etc. Case insensitive. # If not provided, system default will be is assumed. -DefaultEncodingForMSAccessFiles [string] (default: system specific) # Default character encoding for importing CSV (Comma-Separated Value) files. # If not provided, system default will be is assumed. -DefaultEncodingForImportingCSVFiles [string] (default: system locale) # True if options should be visible in the "CSV Export dialog". -ShowOptionsInCSVExportDialog [boolean] (default: false) # If provided, appropriate options for CSV Export Dialog will be loaded -StoreOptionsForCSVExportDialog [boolean] (default: false) # Default delimiter used for exporting CSV (Comma-Separated Value) files. -DefaultDelimiterForExportingCSVFiles [string] (default: ",") # Default text quote character used for exporting CSV (Comma-Separated Value) files. -DefaultTextQuoteForExportingCSVFiles [string] (default: ") # Import missing text values in CSV files as empty text ('' not NULL). -ImportNULLsAsEmptyText [boolean] (default: true) # Default character encoding for exporting CSV (Comma-Separated Value) files. # If not provided, system default will be is assumed. # Only used when StoreOptionsForCSVExportDialog option is true. -DefaultEncodingForExportingCSVFiles [string] (default: UTF-8) # Default setting used to specify whether column names should be added as the first row # for exporting CSV (Comma-Separated Value) files. # Only used when StoreOptionsForCSVExportDialog option is true. -AddColumnNamesForExportingCSVFiles [string] (default: true) # Maximum number of rows that can be displayed in the CSV import dialog. # Used to decrease memory consumption. -MaximumRowsForPreviewInImportDialog [int] (default: 100) # Maximum number of bytes that can be loaded to preview the data in the CSV # import dialog. Used to decrease memory consumption and speed up the GUI. -MaximumBytesForPreviewInImportDialog [int] (default: 10240) Group: Recent Dirs # A list of recently displayed directories in file dialogs related to CSV import/export. -CSVImportExport [URL list] (default: empty) # A list of recently displayed directories in "Source database" file dialog of Project Migration -ProjectMigrationSourceDir [URL list] (default: empty) # A list of recently displayed directories in "Destination database" file dialog of Project Migration -ProjectMigrationDestinationDir [URL list] (default: empty) # A list of recently displayed directories in "Open existing project" and "Create new project" file dialog of Startup Dialog -OpenExistingOrCreateNewProject [URL list] (default: empty) # A list of recent displayed directories in a file dialogs used for dowloading example databases (Get Hot New Stuff) TODO: -DownloadExampleDatabases [URL list] (default: empty) diff --git a/src/widget/utils/kexidatetimeformatter.cpp b/src/widget/utils/kexidatetimeformatter.cpp index af47009de..54c8f3918 100644 --- a/src/widget/utils/kexidatetimeformatter.cpp +++ b/src/widget/utils/kexidatetimeformatter.cpp @@ -1,428 +1,472 @@ /* This file is part of the KDE project Copyright (C) 2006-2016 Jarosław Staniek This program is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "kexidatetimeformatter.h" +#include #include +#include #include #include #include namespace { const QString INPUT_MASK_BLANKS_FORMAT(QLatin1String(";_")); //! Like replace(const QString &before, QLatin1String after) but also returns //! @c true if replacement has been made. bool tryReplace(QString *str, const char *from, const char *to) { Q_ASSERT(str); if (str->contains(QLatin1String(from))) { str->replace(QLatin1String(from), QLatin1String(to)); return true; } return false; } + + //! Settings-related values - cached + class KexiDateFormatterSettings + { + public: + KexiDateFormatterSettings() + { + KConfigGroup generalGroup(KSharedConfig::openConfig(), "General"); + allowTwoDigitYearFormats = generalGroup.readEntry("AllowTwoDigitYearFormats", false); + } + bool allowTwoDigitYearFormats; + bool displayInfoOnce_allowTwoDigitYearFormats = true; + }; + Q_GLOBAL_STATIC(KexiDateFormatterSettings, g_kexiDateFormatterSettings) } + class Q_DECL_HIDDEN KexiDateFormatter::Private { public: Private() // use "short date" format system settings //! @note Qt has broken support for some time formats: https://bugreports.qt.io/browse/QTBUG-59382 //! en_DK should be yyyy-MM-dd but it's dd/MM/yyyy in Qt 5. //! Use en_SE as a workaround. //! @todo allow to override the format using column property and/or global app settings - : inputFormat(QLocale().dateFormat(QLocale::ShortFormat)) + : originalFormat(QLocale().dateFormat(QLocale::ShortFormat)) { - outputFormat = inputFormat; + inputFormat = originalFormat; + outputFormat = originalFormat; //qDebug() << inputFormat << QLocale().dateFormat(QLocale::LongFormat); - emptyFormat = inputFormat; - inputMask = inputFormat; + emptyFormat = originalFormat; + inputMask = originalFormat; computeDaysFormatAndMask(); computeMonthsFormatAndMask(); computeYearsFormatAndMask(); inputMask += INPUT_MASK_BLANKS_FORMAT; } + //! Original format. + const QString originalFormat; + //! Input mask generated using the formatter settings. Can be used in QLineEdit::setInputMask(). QString inputMask; //! Date format used by fromString() and stringToVariant() QString inputFormat; //! Date format used by toString() QString outputFormat; //! Date format used by isEmpty() QString emptyFormat; private: void computeDaysFormatAndMask() { // day (note, order or lookup is important): // - dddd - named days not supported, fall back to "d": if (tryReplace(&inputMask, "dddd", "90")) { // also replace the input format inputFormat.replace(QLatin1String("dddd"), QLatin1String("d")); emptyFormat.remove(QLatin1String("dddd")); return; } // - ddd - named days not supported, fall back to "d": if (tryReplace(&inputMask, "ddd", "90")) { // also replace the input format inputFormat.replace(QLatin1String("ddd"), QLatin1String("d")); emptyFormat.remove(QLatin1String("ddd")); return; } // - dd - The day as a number with a leading zero (01 to 31) // second character is optional, e.g. 1_ is OK if (tryReplace(&inputMask, "dd", "90")) { // also replace the input format inputFormat.replace(QLatin1String("dd"), QLatin1String("d")); emptyFormat.remove(QLatin1String("dd")); return; } // - d - The day as a number without a leading zero (1 to 31); // second character is optional, e.g. 1_ is OK if (tryReplace(&inputMask, "d", "90")) { emptyFormat.remove(QLatin1String("d")); return; } qWarning() << "Not found 'days' part in format" << inputFormat; } void computeMonthsFormatAndMask() { // month (note, order or lookup is important): // - MMMM - named months not supported, fall back to "M" if (tryReplace(&inputMask, "MMMM", "90")) { // also replace the input format inputFormat.replace(QLatin1String("MMMM"), QLatin1String("M")); emptyFormat.remove(QLatin1String("MMMM")); return; } // - MMM - named months not supported, fall back to "M" if (tryReplace(&inputMask, "MMM", "90")) { // also replace the input format inputFormat.replace(QLatin1String("MMM"), QLatin1String("M")); emptyFormat.remove(QLatin1String("MMM")); return; } // - MM - The month as a number with a leading zero (01 to 12) // second character is optional, e.g. 1_ is OK if (tryReplace(&inputMask, "MM", "90")) { // also replace the input format inputFormat.replace(QLatin1String("MM"), QLatin1String("M")); emptyFormat.remove(QLatin1String("MM")); return; } // - M - The month as a number without a leading zero (1 to 12); // second character is optional, e.g. 1_ is OK if (tryReplace(&inputMask, "M", "90")) { emptyFormat.remove(QLatin1String("M")); return; } qWarning() << "Not found 'months' part in format" << inputFormat; } void computeYearsFormatAndMask() { // - yyyy - The year as four digit number. - if (tryReplace(&inputMask, "yyyy", "9999")) { + const char *longDigits = "9999"; + if (tryReplace(&inputMask, "yyyy", longDigits)) { emptyFormat.remove(QLatin1String("yyyy")); return; } - // - yy - The year as four digit number. - if (tryReplace(&inputMask, "yy", "99")) { + const char *shortYearDigits + = g_kexiDateFormatterSettings->allowTwoDigitYearFormats ? "99" : longDigits; + // - yy - The year as two digit number. + if (tryReplace(&inputMask, "yy", shortYearDigits)) { emptyFormat.remove(QLatin1String("yy")); + if (!g_kexiDateFormatterSettings->allowTwoDigitYearFormats) { + // change input format too + inputFormat.replace(QLatin1String("yy"), QLatin1String("yyyy")); + // change output format too + outputFormat.replace(QLatin1String("yy"), QLatin1String("yyyy")); + if (g_kexiDateFormatterSettings->displayInfoOnce_allowTwoDigitYearFormats) { + qInfo() << qPrintable( + QStringLiteral( + "Two-digit year formats for dates are not allowed so KEXI " + "will alter " + "date format \"%1\" by replacing two-digits years with " + "four-digits for accuracy. New input format is \"%2\", new " + "input mask is \"%3\" and new output format is \"%4\".") + .arg(originalFormat, inputFormat, inputMask, outputFormat)) + << "This change will affect input and display. Set the " + "General/AllowTwoDigitYearFormats option to true to enable use of " + "two-digit year formats."; + g_kexiDateFormatterSettings->displayInfoOnce_allowTwoDigitYearFormats = false; + } + } return; } qWarning() << "Not found 'years' part in format" << inputFormat; } }; class Q_DECL_HIDDEN KexiTimeFormatter::Private { public: Private() // use "short date" format system settings //! @todo allow to override the format using column property and/or global app settings : inputFormat(QLocale().timeFormat(QLocale::ShortFormat)) { outputFormat = inputFormat; emptyFormat = inputFormat; inputMask = inputFormat; computeHoursFormatAndMask(); computeMinutesFormatAndMask(); computeSecondsFormatAndMask(); computeMillisecondsFormatAndMask(); computeAmPmFormatAndMask(); inputMask += INPUT_MASK_BLANKS_FORMAT; } ~Private() { } //! Input mask generated using the formatter settings. Can be used in QLineEdit::setInputMask(). QString inputMask; //! Time format used by fromString() and stringToVariant() QString inputFormat; //! Time format used by toString() QString outputFormat; //! Date format used by isEmpty() QString emptyFormat; private: void computeHoursFormatAndMask() { // - hh - the hour with a leading zero (00 to 23 or 01 to 12 if AM/PM display). // second character is optional, e.g. 1_ is OK if (tryReplace(&inputMask, "hh", "90")) { // also replace the input format inputFormat.replace(QLatin1String("hh"), QLatin1String("h")); emptyFormat.remove(QLatin1String("hh")); return; } // the same for HH if (tryReplace(&inputMask, "HH", "90")) { // also replace the input format inputFormat.replace(QLatin1String("HH"), QLatin1String("h")); emptyFormat.remove(QLatin1String("HH")); return; } // - h - the hour without a leading zero (0 to 23 or 1 to 12 if AM/PM display). // second character is optional, e.g. 1_ is OK if (tryReplace(&inputMask, "h", "90")) { emptyFormat.remove(QLatin1String("h")); return; } // the same for H if (tryReplace(&inputMask, "H", "90")) { emptyFormat.remove(QLatin1String("H")); return; } qWarning() << "Not found 'hours' part in format" << inputFormat; } void computeMinutesFormatAndMask() { // - mm - the minute with a leading zero (00 to 59). if (tryReplace(&inputMask, "mm", "90")) { // also replace the input format inputFormat.replace(QLatin1String("mm"), QLatin1String("m")); emptyFormat.remove(QLatin1String("mm")); return; } // - m - the minute without a leading zero (0 to 59). // second character is optional, e.g. 1_ is OK if (tryReplace(&inputMask, "m", "90")) { emptyFormat.remove(QLatin1String("m")); return; } qWarning() << "Not found 'minutes' part in format" << inputFormat; } void computeSecondsFormatAndMask() { // - ss - the second with a leading zero (00 to 59). // second character is optional, e.g. 1_ is OK if (tryReplace(&inputMask, "ss", "90")) { // also replace the input format inputFormat.replace(QLatin1String("ss"), QLatin1String("s")); emptyFormat.remove(QLatin1String("ss")); return; } // - s - the second without a leading zero (0 to 59). // second character is optional, e.g. 1_ is OK if (tryReplace(&inputMask, "s", "90")) { emptyFormat.remove(QLatin1String("s")); return; } //qDebug() << "Not found 'seconds' part in format" << inputFormat; } void computeMillisecondsFormatAndMask() { // - zzz - the milliseconds with leading zeroes (000 to 999). // last two characters are optional, e.g. 1_ is OK if (tryReplace(&inputMask, "zzz", "900")) { // also replace the input format inputFormat.replace(QLatin1String("zzz"), QLatin1String("z")); emptyFormat.remove(QLatin1String("zzz")); return; } // - m - the milliseconds without leading zeroes (0 to 999). // last two characters are optional, e.g. 1_ is OK if (tryReplace(&inputMask, "z", "900")) { emptyFormat.remove(QLatin1String("z")); return; } //qDebug() << "Not found 'milliseconds' part in format" << inputFormat; } void computeAmPmFormatAndMask() { // - AP - interpret as an AM/PM time. AP must be either "AM" or "PM". //! @note not a 100% accurate approach, we're assuming that "AP" substring is only //! used to indicate AM/PM if (tryReplace(&inputMask, "AP", ">AA!")) { // we're also converting to upper case emptyFormat.remove(QLatin1String("AP")); return; } // - ap - interpret as an AM/PM time. ap must be either "am" or "pm". //! @note see above if (tryReplace(&inputMask, "ap", "inputFormat); } QVariant KexiDateFormatter::stringToVariant(const QString& str) const { const QDate date(fromString(str)); return date.isValid() ? date : QVariant(); } bool KexiDateFormatter::isEmpty(const QString& str) const { const QString t(str.trimmed()); return t.isEmpty() || t == d->emptyFormat; } QString KexiDateFormatter::inputMask() const { return d->inputMask; } QString KexiDateFormatter::toString(const QDate& date) const { return date.toString(d->outputFormat); } //------------------------------------------------ KexiTimeFormatter::KexiTimeFormatter() : d(new Private) { } KexiTimeFormatter::~KexiTimeFormatter() { delete d; } QTime KexiTimeFormatter::fromString(const QString& str) const { return QTime::fromString(str, d->inputFormat); } QVariant KexiTimeFormatter::stringToVariant(const QString& str) { const QTime result(fromString(str)); return result.isValid() ? result : QVariant(); } bool KexiTimeFormatter::isEmpty(const QString& str) const { const QString t(str.trimmed()); return t.isEmpty() || t == d->emptyFormat; } QString KexiTimeFormatter::toString(const QTime& time) const { return time.toString(d->outputFormat); } QString KexiTimeFormatter::inputMask() const { return d->inputMask; } //------------------------------------------------ QString KexiDateTimeFormatter::inputMask(const KexiDateFormatter& dateFormatter, const KexiTimeFormatter& timeFormatter) { QString mask(dateFormatter.inputMask()); mask.chop(INPUT_MASK_BLANKS_FORMAT.length()); return mask + " " + timeFormatter.inputMask(); } QDateTime KexiDateTimeFormatter::fromString( const KexiDateFormatter& dateFormatter, const KexiTimeFormatter& timeFormatter, const QString& str) { QString s(str.trimmed()); const int timepos = s.indexOf(' '); const bool emptyTime = timepos >= 0 && timeFormatter.isEmpty(s.mid(timepos + 1)); if (emptyTime) s = s.left(timepos); if (timepos > 0 && !emptyTime) { return QDateTime( dateFormatter.fromString(s.left(timepos)), timeFormatter.fromString(s.mid(timepos + 1)) ); } else { return QDateTime( dateFormatter.fromString(s), QTime(0, 0, 0) ); } } QString KexiDateTimeFormatter::toString(const KexiDateFormatter &dateFormatter, const KexiTimeFormatter &timeFormatter, const QDateTime &value) { if (value.isValid()) return dateFormatter.toString(value.date()) + ' ' + timeFormatter.toString(value.time()); return QString(); } bool KexiDateTimeFormatter::isEmpty(const KexiDateFormatter& dateFormatter, const KexiTimeFormatter& timeFormatter, const QString& str) { int timepos = str.indexOf(' '); const bool emptyTime = timepos >= 0 && timeFormatter.isEmpty(str.mid(timepos + 1)); return timepos >= 0 && dateFormatter.isEmpty(str.left(timepos)) && emptyTime; } bool KexiDateTimeFormatter::isValid(const KexiDateFormatter& dateFormatter, const KexiTimeFormatter& timeFormatter, const QString& str) { int timepos = str.indexOf(' '); const bool emptyTime = timepos >= 0 && timeFormatter.isEmpty(str.mid(timepos + 1)); if (timepos >= 0 && dateFormatter.isEmpty(str.left(timepos)) && emptyTime) { //empty date/time is valid return true; } return timepos >= 0 && dateFormatter.fromString(str.left(timepos)).isValid() && (emptyTime /*date without time is also valid*/ || timeFormatter.fromString(str.mid(timepos + 1)).isValid()); }