QCalendar example illustrating the user-supplied plugin mechanism

This example demonstrates how to write a calendar backend plugin
using a low-level API for extending Qt applications.

Fixes: QTBUG-115200
Change-Id: If0b7f2552ba8c2203acdcbff238fb0ffa7cfca55
Reviewed-by: Edward Welbourne <edward.welbourne@qt.io>
This commit is contained in:
Magdalena Stojek 2024-04-16 10:54:45 +02:00
parent 4079ecfb9b
commit f071d4ee8a
18 changed files with 580 additions and 1 deletions

View File

@ -6,6 +6,7 @@ add_subdirectory(mimetypes)
add_subdirectory(serialization)
add_subdirectory(tools)
add_subdirectory(platform)
add_subdirectory(time)
if(QT_FEATURE_thread)
add_subdirectory(threads)
endif()

View File

@ -6,6 +6,7 @@ SUBDIRS = \
mimetypes \
serialization \
tools \
platform
platform \
time
qtConfig(thread): SUBDIRS += threads

View File

@ -0,0 +1,4 @@
# Copyright (C) 2024 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
add_subdirectory(calendarbackendplugin)

View File

@ -0,0 +1,32 @@
# Copyright (C) 2024 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
cmake_minimum_required(VERSION 3.5)
project(JulianGregorianCalendar VERSION 0.1 LANGUAGES CXX)
find_package(Qt6 REQUIRED COMPONENTS Core)
qt_standard_project_setup()
add_subdirectory(plugin)
add_subdirectory(application)
install(TARGETS calendarPlugin JulianGregorianCalendar
BUNDLE DESTINATION .
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
)
qt_generate_deploy_app_script(
TARGET calendarPlugin
OUTPUT_SCRIPT deploy_script
NO_UNSUPPORTED_PLATFORM_ERROR
)
install(SCRIPT ${deploy_script})
qt_generate_deploy_app_script(
TARGET JulianGregorianCalendar
OUTPUT_SCRIPT deploy_script
NO_UNSUPPORTED_PLATFORM_ERROR
)
install(SCRIPT ${deploy_script})

View File

@ -0,0 +1,23 @@
# Copyright (C) 2024 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
cmake_minimum_required(VERSION 3.5)
project(JulianGregorianCalendar VERSION 0.1 LANGUAGES CXX)
find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Widgets Core)
find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Widgets Core)
qt_standard_project_setup()
include_directories(../common/)
qt_add_executable(JulianGregorianCalendar
../common/calendarBackendInterface.h
main.cpp
)
target_link_libraries(JulianGregorianCalendar
PRIVATE
Qt::Widgets
Qt::Core
)

View File

@ -0,0 +1,11 @@
TEMPLATE = app
TARGET = application
INCLUDEPATH += . \
../common/
QT += core core-private widgets
target.path = $$[QT_INSTALL_EXAMPLES]/corelib/datetime/calendarbackendplugin/application
INSTALLS += target
SOURCES += main.cpp
HEADERS += ../common/calendarBackendInterface.h

View File

@ -0,0 +1,59 @@
// Copyright (C) 2024 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
#include "calendarBackendInterface.h"
#include <QApplication>
#include <QCalendar>
#include <QCalendarWidget>
#include <QCommandLineParser>
#include <QPluginLoader>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QCoreApplication::setApplicationName("JulianGregorianCalendar");
QCommandLineParser parser;
parser.setApplicationDescription("Calendar Backend Plugin Example");
parser.addHelpOption();
parser.addPositionalArgument("date; names",
"Date of transition between "
"Julian and Gregorian calendars "
"as string in the format 'yyyy-MM-dd;'. Optionally, user can "
"provide names for the calendar separated with ';'");
parser.process(a);
const QStringList args = parser.positionalArguments();
if (args.isEmpty())
parser.showHelp(1);
if (args.at(0).isEmpty())
parser.showHelp(1);
//![0]
QPluginLoader loader;
loader.setFileName("../plugin/calendarPlugin");
loader.load();
if (!loader.isLoaded())
return 1;
auto *myplugin = qobject_cast<RequestedCalendarInterface*>(loader.instance());
//![0]
//![1]
const auto cid = myplugin->loadCalendar(args.at(0));
if (!cid.isValid()) {
qWarning() << "Invalid ID";
parser.showHelp(1);
}
const QCalendar calendar(cid);
//![1]
//![2]
QCalendarWidget widget;
widget.setCalendar(calendar);
widget.show();
QCalendar::YearMonthDay when = { 1582, 10, 4 };
QCalendar julian = QCalendar(QCalendar::System::Julian);
auto got = QDate::fromString(args.at(0).left(10), u"yyyy-MM-dd", julian);
if (got.isValid())
when = julian.partsFromDate(got);
widget.setCurrentPage(when.year, when.month);
//![2]
return a.exec();
}

View File

@ -0,0 +1,4 @@
TEMPLATE = subdirs
SUBDIRS += plugin \
application

View File

@ -0,0 +1,27 @@
// Copyright (C) 2024 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
#ifndef CALENDARINTERFACE_H
#define CALENDARINTERFACE_H
#include <QCalendar>
#include <QObject>
//![0]
class RequestedCalendarInterface
{
public:
RequestedCalendarInterface() = default;
virtual QCalendar::SystemId loadCalendar(QAnyStringView requested) = 0;
virtual ~RequestedCalendarInterface() = default;
};
//![0]
QT_BEGIN_NAMESPACE
//![1]
#define RequestedCalendarInterface_iid \
"org.qt-project.Qt.Examples.CalendarBackend.RequestedCalendarInterface/1.0"
Q_DECLARE_INTERFACE(RequestedCalendarInterface, RequestedCalendarInterface_iid)
//![1]
QT_END_NAMESPACE
#endif // CALENDARINTERFACE_H

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

@ -0,0 +1,186 @@
// Copyright (C) 2024 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GFDL-1.3-no-invariants-only
/*!
\example time/calendarbackendplugin
\title Calendar Backend Plugin Example
\examplecategory {Data Processing & I/O}
\ingroup examples-time
\brief QCalendar example illustrating user-supplied custom calendars.
\image calendarwindow_transition.png
\section1 Introduction
There are numerous different calendar systems in use around the globe.
Qt has built-in support for some of them (see \l{QCalendar::}{System}),
but can't provide general support due to their high number.
Additional calendar systems can be provided by implementing a
custom QCalendarBackend, which is a private API.
This example demonstrates how to write a custom calendar backend
and how to use the low-level plugin API to extend an application
to user-selectable calendars.
Many countries transitioned from the Julian to the Gregorian calendar
at some point in their history, and this custom calendar backend will
implement the respective calendar as an example. The custom backend
is compiled into a plugin and loaded at runtime by the main application.
The exact transition date, different for various regions, is provided
as a string to the plugin and can be determined by the user.
\section1 Calendar backend
The calendar backend class must inherit from \c QCalendarBackend and implement its
pure virtual functions in a \c thread-safe way. It may also override
some other virtual functions as needed.
\section2 Example implementation
This example inherits from the already existing \c QRomanCalendar,
which in turn inherits from the \c QCalendarBackend and implements some of
its virtual functions.
It's constructive to do this because the transition calendar shares,
with both Julian and Gregorian calendars, parts provided by the Roman calendar.
Here is the class declaration of \c JulianGregorianCalendar:
\snippet time/calendarbackendplugin/plugin/calendarbackend.h 0
The \c QDate passed to the constructor - \a endJulian - is the date of the last day
of the Julian calendar. The calendar will automatically calculate the shift for a given
year, e.g in 1582, 10 days were omitted, but in 1700, 12 days had to be omitted.
The calendar backend is registered under \a name and a calendar instance can be
created using that name. The class only overrides functions where the two calendars
it combines differ from the Roman base. It has instances of the Julian and Gregorian
calendars to which these functions can delegate.
\section2 Julian Day conversions
\c dateToJulianDay(int year, int month, int day, qint64 *jd) computes the Julian
day number corresponding to the specified \a year, \a month and \a day.
Returns \c true and sets \a jd if there is such a date in this calendar; otherwise,
returns \c false.
\snippet time/calendarbackendplugin/plugin/calendarbackend.cpp 0
\c julianDayToDate(qint64 jd) computes year, month and day in this calendar for the given
Julian day number, \a jd. If the given day falls outside this calendar's scope,
the return value for \c isValid() is \c false. In this example, if the given date
falls in the gap jumped over by the transition from Julian to Gregorian calendar,
it is out of scope.
\snippet time/calendarbackendplugin/plugin/calendarbackend.cpp 1
\section2 Locale support
A calendar may, in general, have its own naming of months of the year
and days of the week. These must be suitably localized to be intelligible
to all users. By default the backend baseclass takes care of week day
names for us, which is entirely sufficient for these Julian/Gregorian
transition calendars.
Although a backend can directly override the month naming methods, the
baseclass version of these can be customized by implementing
\c localeMonthData() and \c localeMonthIndexData() to provide tables
of localized month names. Since the Julian and Gregorian
calendars use the same month naming, they inherit that customization
from a common base, \c QRomanCalendar. This also means the custom
calendar can use the same names, again by inheriting from that base.
This takes care of localization.
\sa QCalendarWidget, QCalendar, QDate, QLocale
\section1 Plugin
Qt applications can be extended through plugins. This requires the application to
detect and load plugins using \l{QPluginLoader}.
\section2 Writing a plugin
To write a plugin, the first thing that has to be done is to
create a pure virtual class that defines the interface between
plugin and application.
In this example the following interface was used:
\snippet time/calendarbackendplugin/common/calendarBackendInterface.h 0
and register it in the Qt meta-object system:
\snippet time/calendarbackendplugin/common/calendarBackendInterface.h 1
\l{<QtPlugin>::}{Q_DECLARE_INTERFACE()} macro is used to associate the
\c ClassName (here: \c RequestedCalendarInterface) with the defined
\c Identifier (here: \c RequestedCalendarInterface_iid). The \c Identifier
must be unique. This interface can be implemented by plugins that load
other calendars, interpreting \c loadCalendar()'s string parameter in
various ways. It isn't limited to this particular plugin that will be implemented
using it, so it has a generic name, not one specific to this particular backend.
Then a plugin class that inherits from \l{QObject} and from the interface is created.
\snippet time/calendarbackendplugin/plugin/calendarplugin.h 0
\l{<QtPlugin>::}{Q_PLUGIN_METADATA()} and \l{QObject::}{Q_INTERFACES()}
are being used to declare meta data that was also declared
in the interface class and to tell Qt which interface the class implements.
\sa QtPlugin
This plugin instantiates and registers a custom calendar backend which
can in turn be used to instantiate \l{QCalendar} by the application at
any point.
Qt Plugins are stored in a single shared library (a DLL) and \l{QPluginLoader}
is used for detecting and dynamically loading the plugin file (for more
see \l{How to Create Qt Plugins}).
\section2 Loading the plugin
\l{QPluginLoader} checks if the plugin's version of Qt
is the same as that of the application and
provides direct access to a Qt plugin.
Here is the use of \l QPluginLoader in the example:
\snippet time/calendarbackendplugin/application/main.cpp 0
First, an instance of a QPluginLoader object needs to be initialized. Next,
it has to be specified which plugin to load by passing a DLL file name to
\l{QPluginLoader::}{setFileName()}. Then, by using \l{QPluginLoader::}{load()},
the plugin file is dynamically loaded. At the end, a call to \l{QObject::}{qobject_cast()}
tests whether a plugin implements a given interface. \l{QObject::}{qobject_cast()}
uses \l{QPluginLoader::}{instance()} to access the root component in the plugin.
If the plugin has been loaded correctly, its functions should be available.
\sa QPluginLoader
\section2 Instantiating the backend
In this example there is only one function in the plugin. \c loadCalendar()
is responsible for registering the custom calendar backend in
\c QCalendarRegistry with given date of the transition and names.
\snippet time/calendarbackendplugin/plugin/calendarplugin.cpp 0
String argument for \c loadCalendar() is supplied by the user via command
line arguments. Then, the date of transition from the Julian calendar to
the Gregorian is extracted by splitting the given string.
After validation, a custom backend object is created.
The backend must be registered before it can be used in \l{QCalendar},
using the \c registerCustomBackend() method.
Once a backend is registered, a QCalendar can be instantiated with
the respective \l{QCalendar::}{SystemId} or \c name.
Here is the use of \c loadCalendar in the \c main:
\snippet time/calendarbackendplugin/application/main.cpp 1
\section2 Extending QCalendarWidget
By creating a \l{QCalendar} instance with a specific calendar as a backend,
it is possible to provide \l{QCalendarWidget} with that backend and
visualize it.
\snippet time/calendarbackendplugin/application/main.cpp 2
*/

View File

@ -0,0 +1,27 @@
# Copyright (C) 2024 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
cmake_minimum_required(VERSION 3.5)
project(calendarPlugin VERSION 0.1 LANGUAGES CXX)
find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Widgets Core)
find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Widgets Core)
qt_standard_project_setup()
include_directories(../common/)
qt_add_library(calendarPlugin SHARED
../common/calendarBackendInterface.h
calendarplugin.h
calendarplugin.cpp
calendarbackend.cpp
calendarbackend.h
)
target_link_libraries(calendarPlugin
PRIVATE
Qt::Widgets
Qt::Core
Qt::CorePrivate
)

View File

@ -0,0 +1,103 @@
// Copyright (C) 2024 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
#include "calendarbackend.h"
#include <QCalendar>
JulianGregorianCalendar::JulianGregorianCalendar(QDate endJulian, QAnyStringView name = {})
: m_julianUntil(julian.partsFromDate(endJulian)),
m_gregorianSince(gregorian.partsFromDate(endJulian.addDays(1))),
m_name(name.isEmpty()
? endJulian.toString(u"Julian until yyyy-MM-dd", julian)
: name.toString())
{
Q_ASSERT_X(m_julianUntil.year < m_gregorianSince.year
|| (m_julianUntil.year == m_gregorianSince.year
&& (m_julianUntil.month < m_gregorianSince.month
|| (m_julianUntil.month == m_gregorianSince.month
&& m_julianUntil.day < m_gregorianSince.day))),
"JulianGregorianCalendar::JulianGregorianCalendar()",
"Perversely early date for Julian-to-Gregorian transition");
}
QString JulianGregorianCalendar::name() const
{
return QStringLiteral("JulianGregorian");
}
int JulianGregorianCalendar::daysInMonth(int month, int year) const
{
if (year == QCalendar::Unspecified)
return QRomanCalendar::daysInMonth(month, year);
if (year < m_julianUntil.year
|| (year == m_julianUntil.year && month < m_julianUntil.month)) {
return julian.daysInMonth(month, year);
}
if ((year > m_gregorianSince.year)
|| (year == m_gregorianSince.year && month > m_gregorianSince.month)) {
return gregorian.daysInMonth(month, year);
}
if (m_julianUntil.year == m_gregorianSince.year) {
Q_ASSERT(year == m_julianUntil.year);
if (m_julianUntil.month == m_gregorianSince.month) {
Q_ASSERT(month == m_julianUntil.month);
return QRomanCalendar::daysInMonth(month, year)
+ m_julianUntil.day - m_gregorianSince.day + 1;
}
}
if (year == m_julianUntil.year && month == m_julianUntil.month)
return m_julianUntil.day;
if (year == m_gregorianSince.year && month == m_gregorianSince.month)
return gregorian.daysInMonth(month, year) + 1 - m_gregorianSince.day;
Q_ASSERT(year > 3900);
return 0;
}
bool JulianGregorianCalendar::isLeapYear(int year) const
{
if (year < m_julianUntil.year
|| (year == m_julianUntil.year
&& (m_julianUntil.month > 2
|| (m_julianUntil.month == 2 && m_julianUntil.day == 29)))) {
return julian.isLeapYear(year);
}
return gregorian.isLeapYear(year);
}
//![0]
bool JulianGregorianCalendar::dateToJulianDay(int year, int month, int day, qint64 *jd) const
{
if (year == m_julianUntil.year && month == m_julianUntil.month) {
if (m_julianUntil.day < day && day < m_gregorianSince.day) {
// Requested date is in the gap skipped over by the transition.
*jd = 0;
return false;
}
}
QDate givenDate = gregorian.dateFromParts(year, month, day);
QDate julianUntil = julian.dateFromParts(m_julianUntil);
if (givenDate > julianUntil) {
*jd = givenDate.toJulianDay();
return true;
}
*jd = julian.dateFromParts(year, month, day).toJulianDay();
return true;
}
//![0]
//![1]
QCalendar::YearMonthDay JulianGregorianCalendar::julianDayToDate(qint64 jd) const
{
const qint64 jdForChange = julian.dateFromParts(m_julianUntil).toJulianDay();
if (jdForChange < jd) {
QCalendar gregorian(QCalendar::System::Gregorian);
QDate date = QDate::fromJulianDay(jd);
return gregorian.partsFromDate(date);
} else if (jd <= jdForChange) {
QCalendar julian(QCalendar::System::Julian);
QDate date = QDate::fromJulianDay(jd);
return julian.partsFromDate(date);
}
return QCalendar::YearMonthDay(QCalendar::Unspecified, QCalendar::Unspecified,
QCalendar::Unspecified);
}
//![1]

View File

@ -0,0 +1,29 @@
// Copyright (C) 2024 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
#ifndef CALENDARBACKEND_H
#define CALENDARBACKEND_H
#include "private/qromancalendar_p.h"
#include "qdatetime.h"
#include <QtCore/private/qcalendarbackend_p.h>
//![0]
class JulianGregorianCalendar : public QRomanCalendar
{
public:
JulianGregorianCalendar(QDate endJulian, QAnyStringView name);
QString name() const override;
int daysInMonth(int month, int year = QCalendar::Unspecified) const override;
bool isLeapYear(int year) const override;
bool dateToJulianDay(int year, int month, int day, qint64 *jd) const override;
QCalendar::YearMonthDay julianDayToDate(qint64 jd) const override;
private:
static inline const QCalendar julian = QCalendar(QCalendar::System::Julian);
static inline const QCalendar gregorian = QCalendar(QCalendar::System::Gregorian);
QCalendar::YearMonthDay m_julianUntil;
QCalendar::YearMonthDay m_gregorianSince;
QString m_name;
};
//![0]
#endif // CALENDARBACKEND_H

View File

@ -0,0 +1,32 @@
// Copyright (C) 2024 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
#include "calendarplugin.h"
JulianGregorianPlugin::JulianGregorianPlugin()
{
}
//![0]
QCalendar::SystemId JulianGregorianPlugin::loadCalendar(QAnyStringView request)
{
Q_ASSERT(!request.isEmpty());
QStringList names = request.toString().split(u';');
if (names.size() < 1)
return {};
QString dateString = names.takeFirst();
auto date = QDate::fromString(dateString, u"yyyy-MM-dd",
QCalendar(QCalendar::System::Julian));
if (!date.isValid())
return {};
QString primary = names.isEmpty() ?
QString::fromStdU16String(u"Julian until ") + dateString : names[0];
auto backend = new JulianGregorianCalendar(date, primary);
names.emplaceFront(backend->name());
auto cid = backend->registerCustomBackend(names);
return cid;
}
JulianGregorianPlugin::~JulianGregorianPlugin()
{
}
//![0]

View File

@ -0,0 +1,25 @@
// Copyright (C) 2024 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
#ifndef CALENDARPLUGIN_H
#define CALENDARPLUGIN_H
#include "calendarbackend.h"
#include "calendarBackendInterface.h"
#include <QtPlugin>
//![0]
class JulianGregorianPlugin : public QObject, public RequestedCalendarInterface
{
Q_OBJECT
Q_INTERFACES(RequestedCalendarInterface)
Q_PLUGIN_METADATA(IID "org.qt-project.Qt.Examples."
"CalendarBackend."
"RequestedCalendarInterface/1.0")
public:
JulianGregorianPlugin();
QCalendar::SystemId loadCalendar(QAnyStringView request) override;
~JulianGregorianPlugin();
};
//![0]
#endif // CALENDARPLUGIN_H

View File

@ -0,0 +1,11 @@
TEMPLATE = lib
TARGET = calendarPlugin
INCLUDEPATH += . \
../common/
QT += core core-private widgets
HEADERS += calendarbackend.h calendarplugin.h
SOURCES += calendarbackend.cpp calendarplugin.cpp
target.path = $$[QT_INSTALL_EXAMPLES]/corelib/datetime/calendarbackendplugin/plugin
INSTALLS += target

View File

@ -0,0 +1,4 @@
TEMPLATE = subdirs
CONFIG += no_docs_target
SUBDIRS = calendarbackendplugin