From f071d4ee8abf6fd0f1b6e187b4e99fa6fad7b642 Mon Sep 17 00:00:00 2001 From: Magdalena Stojek Date: Tue, 16 Apr 2024 10:54:45 +0200 Subject: [PATCH] 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 --- examples/corelib/CMakeLists.txt | 1 + examples/corelib/corelib.pro | 3 +- examples/corelib/time/CMakeLists.txt | 4 + .../time/calendarbackendplugin/CMakeLists.txt | 32 +++ .../application/CMakeLists.txt | 23 +++ .../application/application.pro | 11 ++ .../application/main.cpp | 59 ++++++ .../calendarbackendplugin.pro | 4 + .../common/calendarBackendInterface.h | 27 +++ .../doc/images/calendarwindow_transition.png | Bin 0 -> 22607 bytes .../doc/src/calendarbackendplugin.qdoc | 186 ++++++++++++++++++ .../plugin/CMakeLists.txt | 27 +++ .../plugin/calendarbackend.cpp | 103 ++++++++++ .../plugin/calendarbackend.h | 29 +++ .../plugin/calendarplugin.cpp | 32 +++ .../plugin/calendarplugin.h | 25 +++ .../calendarbackendplugin/plugin/plugin.pro | 11 ++ examples/corelib/time/time.pro | 4 + 18 files changed, 580 insertions(+), 1 deletion(-) create mode 100644 examples/corelib/time/CMakeLists.txt create mode 100644 examples/corelib/time/calendarbackendplugin/CMakeLists.txt create mode 100644 examples/corelib/time/calendarbackendplugin/application/CMakeLists.txt create mode 100644 examples/corelib/time/calendarbackendplugin/application/application.pro create mode 100644 examples/corelib/time/calendarbackendplugin/application/main.cpp create mode 100644 examples/corelib/time/calendarbackendplugin/calendarbackendplugin.pro create mode 100644 examples/corelib/time/calendarbackendplugin/common/calendarBackendInterface.h create mode 100644 examples/corelib/time/calendarbackendplugin/doc/images/calendarwindow_transition.png create mode 100644 examples/corelib/time/calendarbackendplugin/doc/src/calendarbackendplugin.qdoc create mode 100644 examples/corelib/time/calendarbackendplugin/plugin/CMakeLists.txt create mode 100644 examples/corelib/time/calendarbackendplugin/plugin/calendarbackend.cpp create mode 100644 examples/corelib/time/calendarbackendplugin/plugin/calendarbackend.h create mode 100644 examples/corelib/time/calendarbackendplugin/plugin/calendarplugin.cpp create mode 100644 examples/corelib/time/calendarbackendplugin/plugin/calendarplugin.h create mode 100644 examples/corelib/time/calendarbackendplugin/plugin/plugin.pro create mode 100644 examples/corelib/time/time.pro diff --git a/examples/corelib/CMakeLists.txt b/examples/corelib/CMakeLists.txt index 38a883b4ea7..856e1f30294 100644 --- a/examples/corelib/CMakeLists.txt +++ b/examples/corelib/CMakeLists.txt @@ -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() diff --git a/examples/corelib/corelib.pro b/examples/corelib/corelib.pro index 625957ca1a6..d921ae9ed37 100644 --- a/examples/corelib/corelib.pro +++ b/examples/corelib/corelib.pro @@ -6,6 +6,7 @@ SUBDIRS = \ mimetypes \ serialization \ tools \ - platform + platform \ + time qtConfig(thread): SUBDIRS += threads diff --git a/examples/corelib/time/CMakeLists.txt b/examples/corelib/time/CMakeLists.txt new file mode 100644 index 00000000000..32b93e9ecfa --- /dev/null +++ b/examples/corelib/time/CMakeLists.txt @@ -0,0 +1,4 @@ +# Copyright (C) 2024 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +add_subdirectory(calendarbackendplugin) diff --git a/examples/corelib/time/calendarbackendplugin/CMakeLists.txt b/examples/corelib/time/calendarbackendplugin/CMakeLists.txt new file mode 100644 index 00000000000..7aa4a18a1e5 --- /dev/null +++ b/examples/corelib/time/calendarbackendplugin/CMakeLists.txt @@ -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}) diff --git a/examples/corelib/time/calendarbackendplugin/application/CMakeLists.txt b/examples/corelib/time/calendarbackendplugin/application/CMakeLists.txt new file mode 100644 index 00000000000..3cd4ffe430c --- /dev/null +++ b/examples/corelib/time/calendarbackendplugin/application/CMakeLists.txt @@ -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 +) diff --git a/examples/corelib/time/calendarbackendplugin/application/application.pro b/examples/corelib/time/calendarbackendplugin/application/application.pro new file mode 100644 index 00000000000..c6a268c9a05 --- /dev/null +++ b/examples/corelib/time/calendarbackendplugin/application/application.pro @@ -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 diff --git a/examples/corelib/time/calendarbackendplugin/application/main.cpp b/examples/corelib/time/calendarbackendplugin/application/main.cpp new file mode 100644 index 00000000000..e270bb194b2 --- /dev/null +++ b/examples/corelib/time/calendarbackendplugin/application/main.cpp @@ -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 +#include +#include +#include +#include + +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(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(); +} diff --git a/examples/corelib/time/calendarbackendplugin/calendarbackendplugin.pro b/examples/corelib/time/calendarbackendplugin/calendarbackendplugin.pro new file mode 100644 index 00000000000..abfca31aead --- /dev/null +++ b/examples/corelib/time/calendarbackendplugin/calendarbackendplugin.pro @@ -0,0 +1,4 @@ +TEMPLATE = subdirs + +SUBDIRS += plugin \ + application diff --git a/examples/corelib/time/calendarbackendplugin/common/calendarBackendInterface.h b/examples/corelib/time/calendarbackendplugin/common/calendarBackendInterface.h new file mode 100644 index 00000000000..194bd22714f --- /dev/null +++ b/examples/corelib/time/calendarbackendplugin/common/calendarBackendInterface.h @@ -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 +#include + +//![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 diff --git a/examples/corelib/time/calendarbackendplugin/doc/images/calendarwindow_transition.png b/examples/corelib/time/calendarbackendplugin/doc/images/calendarwindow_transition.png new file mode 100644 index 0000000000000000000000000000000000000000..eb5e2ba0c86b0a953c46cde38aeda707b90dee9c GIT binary patch literal 22607 zcmd43byOTtw=W1GNYD`66G$Mqy9EpG?he7-Ed=+VjYH7j?(Pl=?(Wt=>aWrH?WEdD2G)W0jB^a1jp)fG8^+*W7H;;ea z?ty<^I|)mwAOV{1odPc+%@+h}E#kn(=MR&w2YqIj#R^@F@77UGe~*UVZ|WA3XNS2JaF zM&+07cr;t4jX}b53H7`>RJ{T=c2qb_V7t3vl8s+pUOtW@;ym@dno*}>&}((atVHC3 zwJ&~x4+##|X$z|}c%abVK0zHnUjafROq@*lR z8;B;ctBxTPgw6~O4kD|CynDGJs|9P0p0zc>KWYUsDZrO6-(eo56i8kj&KQ$yY-||* z^SjuN{^e`L-RiRBbLzKQ$a1`2C19rMLlil2bM=nJ${6YB=(;6jWY}D2iGT)TjVJ|h zZfdzk#}FABlNF2D>YMcL#PixLtN3cT3BA=GlNRGYrWq-JWavACQk9K=1mUlIq& z>FpkP7eu$$24!7`$sS4lP<=cvaY0*No4dVLsE*L8tBAo*SXrKmJ8wJfn;bfrG0@3q zCogD1$v3IZ2^g1G!=Pj~A267v)S0TcWS7|;;Vsk^j4z8%1h%{!4@|3Xi)L#NIWnV~ zo(EAv2S05?P42`Jdz42@t(UQqa4j?k#42<}!$GZRt$*)e1u(pl39vjdy{gDi(5Tv> z(Azt1#=lfj@sSyjyW3nwjr$SXcdRn6q(FscTj-a5rL1*ut=r&^XSk(|Ca(`!GLMm; zI*!Si;iN=W3-lmf_*`d{!{hdnJ1%KNH^!%tH_2*d ztB`H$g^9nItPX{yum&jCh1<%yXJzXCA}9KfkUQQj=7uMKMb_OP6}dk?ljE7ZW;foi zwPX0ZC=>nk$ng{+V;YmoGPdNr7p>$viFIAcEdWI@TEnost840MkM9gTQ}^5` z#THxj->ymGn+XGH&G!YbnV{il&U(S?#GpH5}IX}))9Rz%8VWPQM)aS5KpQx(`V zsW`rL1BTv)yGdqM=4?l+miOYgFfgVa&pc={Fs2z#2FtupLMN<<*u(a4fmX8vubHD|{pk1m@WDQ2skc+v z^lg@6?~F(d&IicO52Uw#L#{@a*JaG+DtIO7=KRGU=K7aevtrDlG3FvxOipL2^Ec%t zJ9?a5HqC#}b%6PSGZCEF;h1fWNP41;4gm$C7U@|e72yW^67bq&)O@> zcJ!VsB)r}_A`slCHY=FhEEkY>P}O)B(cdwEp^g62R9Nzcv;duG*sBASYf~XK?fN_n zQtoH{*fmZCzRvD$(yNJ#T4VU@%8TAxW#q=-&*{2%>Xg_cJm=S6?a`9La{Ov*quHJ` zD;=%%xi`W)Rw;n@VNHbny4tzlj+iMTZim%jKi)FqPioGc(6Rp=P;P^ z9*d8H+yv@Izyp%x)!YBa(-Esy-q~GMf$fxS8)bhyyVkq@*x2+-2l{RtdCp;vg#OGy zb!RZ<=KP>Q_G4oOIS+df+thoyYHUAO()dXI4uj3=-09P4(5xcWoZ>mpX=@w2z?aov zPIpf$hB{K;VE+uGs-~mZx^`AOdhIUojcV>=@SV3?=lbCX3Vctuj~q9)*M9e~g6ZBt ztz%Bfb)P_l7}4uZUbF_e6jv)94&VWO$VqfLpU%$l+(*u6P0+WNkvx)5Gd2`-G;xsW z9`ds1gjRZ|yZcvL4vtRaS>{vZxF?*yp9ZD*{?^cOL~k_s;tJYzgxc`?l#2E`nq{J} z?oEu$kEZht^52-+oG!NtPn4+%Vc*R?Kqld0K1S#p4y2+@UxY9kg zQjWqgDiw&#zT#5JR0bt|>}8UalcEGEi?hjAyVCE}qdAzIYO?Ou;rBLjSFXBDrzezxe; z!BL#7^D%#`eVx-$IobP!(Qk7|)=SmWEXVU7A7`9>%RQ(P zFRtT$9w0o&PcV{*QB#EYbUgdFn2K3%S3Ng8HCpF~(%1-skMxR9>Jx1wdRRE>`^L79 z)++J`G(+Yh>Y>VO@!kCaHMXi-DFc|8lF6|>b}iTObJfEHVE>2}HB9i0yq>Ce6G5HP zI8EpV;}zZKkf9zb*tg90tB-NFj`HXj?$4zz*k9olxAph91Us0&-$YOErWSu1(hv+W zC6Fs-YY(@FJ1Y_k;PIt~j+D2+Iax*b%Zk^?8-FkvxndBPRy!&=<@!Tam*VfCn?2$mUq`^@&fvl-W@-^F?APvEnrf5`O1GJu1znG9yZ@ivLV^$e!OsXvX2XvmaeR;xw3+ZiH>2TRv*XQ!@?nw4m+OZ-1cpXA53$^@PbebRQPKktZ)$F^C^ zmiHM~I8$5-9|<0LDGj<~;}(wFENeonRT`}9vlJT7Ip1IYl_M&Ji!aQHk z4DU@fvd}FZG7o8ThcENL0Yax`*J}6nMm&;V6V5nwlR_`2OZ9K{rIT*+??Iqv3MbOjpH?HLUO7zEceIWFJ{KhZ!%~zgO_8 z@90Rk^OMO55f@_n>@m`Vx^hNC=6A&sP}pdH8=}xGg_RTG{47j_$tf@r*ig)BIH{xN zbM+yjTE_d8v{Jh*mSv`$U8X$!eJeA3PFr8SFs~FxoUpRJ492zwQm^9 z^_B3jLYuFt)FSk>K73G6Qz9yr8Gj~+|1`+`-UnUYq0l6~Ztrd9Z(Q9x!G^+)K05Ng zERVoOYSwuyh3L?C_9iRpGN~;Q$%vGY^Jmq`tjKqTkZBgC0kv0p1vO4 zHeRna4t)J3@pEL!h%sBK^WP(tf3$#!!NV^O5Mxokf1VVIh$9L#jCr07P$gg^Re380 z?ClLe&HwT7qtCI%quhv(uql&;95Kzxc=LZEVqK!#z zYVG>teZbpEe`XTuPG~0~;VN%O7O(%(n2_A8JdjDfe!Xcw?~sC52T>H=uC`x9E1>}T zh$a}lwRI$7VDR?kAg!QMS|v%k+V`1y=oxZk=zU}+U7Me*-DX-oc8Q5H1R3}d)gX#8 z*Tu;BU}AEc29z*E@aVP`9);57vdBTbD5nT~>fGu4H$+KfRw~=Z^W=`EminCETZj^~ zEiI^;80Ig*FioDK>|0;G`@VE%1F`;WP49L2V$Q5g)8rQddJ%-R$Y!5;Pz+e zLPNCRAzk6l0$MJp-v~dwCHBVb^1ep%7 zlJdKR-0qwNz^(A-!aHZ)v{;z1`13r0nRBm=Uisy1Z-CjpX${3t;yk(gx+0_0o9Le1 z#df*UeC4uVWRzd^8)nDx@yIr1axwBAPsQ5?`N5dGDKThc?7Xu7OkZKJ+IWh2%Z-L> z%P*IKPj=6NK~6u?mt!+Pu)vH8S~7Wp5?c;%9jt!*xs8ga>^s`%O+-cB*DvJekwmtY zBeltmVuac;MSf;}+R`BJ%gyKfozZ;4Ow0@mJ!Plps3xWOdhFP7#!y) z+aU4hS&iD_4f#3=s+M<7MT7^J$#|Pt`fu@U_{Rqv2KaVWBb?tI+Zk`nZnd&;23gVE zUAW#LS$=&uTbM{e&GZP&2;(*}ORFt9nHX^kR^&fCen&&Dp45H6Q0>fFUeU2zj!yDf zxq$d(JnN9j8hA9P3I5)%Dz6zAx}*qbF7MNsE2Wk*hhn6IaYxOKJvGr&?&QlY*GA6< zf)!@k@6uz$wufh0i^&iwz4y+4I3k@nEHk5%Ibm+vn;xI4_M+?O0+NZY8BfoJ~8z}$#oPW61NO%F5S>HO&z4cL=b{~kt$1BJBl&Ujb}G}_S04Hh zQcP9O({E!(#pDNB@RI3~-hRGqYf2KFSk{W0Wq;zX!|9;fi%T?~%qb?*B}~lTWB1=~ zl5Jl}=*YZ!s2Lg^7ffF+ttNvZLdW*q*As=VARn2BjD}mwFpjvP?82=(-`nff6imC& z3s~uzp9t{@y+3M#UF(xZ!idPWUeGu_xFb_q^+=;wi2b-&Tsfi|^0y+!6h!N1w~_Ml zz+8%NiDk~|G;Fb;5-y4~gK`>gc+t~@ZYce?8)ZCw5=51nk4jyhD)(M*CGBPf>{k*v zTWb8kYN7$b{<)DmHpAVGz)GaXob~uH@Or_p9X}B27R1P5_R~q z19^hhx{p!0DYgY=_^gB1OJDa$Fw2l& z-aagN%-^c}*^T{kSAn3TyY(*S#d7l(v27_gGcw*Mhl5sKHrLYa*bsz`t*svafQMuj z7ec`n?YQmVp0my4UTJ9`#Z_k;Qz7S6@!a+Xi~<7fd>r;~(MC;0w@+^oIiy9QXq(sD zpiH9T2R!0-_{N*X_fqf++^*eYK$6X*=L`najBr3Yaz7>UoF@NcYjGOJ{$a%>Z}mf!S#=ohJv{%RCtlC+>xMh&JrU?S*NNB1*fx0e+{6(&&m=DNm*3^{V`hOr zj7hpZYpD(N!#-cb(4d_%{Q@rLx!#9`4#l;$WU64nu$UOehnwf|5Hg=pgn=<)7d1gJ zGEpipC+f^l36|$)?is=8Y@mt|Q%2o-0AtqoWDF zcg=^0EZ>oSvu%B95)vcx6{vOQ9ZgW4F@rV2G~&+Ewb+NL$)mqOKb<7tJT+Ua1JXft2BZ}tMh!b_!hG)Dn--62; z0iyvK_C3d_5P)7Y9cAFWu~Yjz=7di+C!qnjBiBe(R;;7@ND$v2}|uFF1fjyPfDvc8Y(ypW}n z`bh5RKttvkg4nVmEMfl^OE6`${<~`)JhtzOL@WyVm$1={z(Tq%v)wq2L-gne)mz>a zG0|2ny;;RgPz(hlEj*0)7s(uL^iIsTW)3Wvu{r00RiMcsNL34qL}X*s=sUbsC--47 zfWXGmjupir{U#0{#OXM8T>^VECAo@1{yK06RI@2FTG_j)FtVNPL&nlN*$s-Jl z(`z)E6es-WyEsyXYFa2V&Ks-MS8EG3Wznj1-p@}rqGBM!rFt9lu{2h5hJ1?L+M9eN zTm~)CgTdcUzWkSedejCWmW-`ucUvz~P(RAe+TY!qda~a~7&gy>_QvrHf!S(S(XG?kk>wm&akT@lIWP zTYFL%4#&$v_X$IdBEk;Gi(w@?+$F|1DCg&I8dq$;MiLKA z%L~#cXM#Wjp|KROT;8`#fRs@aqr=sz|JGG;GzZ7zabeP~UHRRD%I^c>>(?3tV@ESZ z%4zbHvfU@H#x*;BYGgc@4p6R3#Hm7AS`Awut#R4(>|0`lixR`nk?i|)4|_J48>3WY zB!+-Niuz?J?$_~r)zq+i-(n~xqmy|EI(*E;4Y*k`_bZ#_^uA-?h2=HB7t-lkR^(n5 z#a@zjWqzK)S{2sRz}}AuR!7IqlXe+ultTW{zGUM8bVsc<8QY%V#$px97G2(p!)e&; z-cZcfYiorDnpyeMZz>eAU>;W65?b%fx~-=3R2Ccqb=^qD$$V=~h)UxU5=5WwuWkuN$QfUw)X`a$V^q*OMZRA{Lv)TGO83DEK4Z+X1rWm3&&$k-o{_4p?gCS}u2- z!{l9EUGQLDK|y_~gwKj+^J{Az22BNH<(~?yxy##5d3?F2 zi;2s3&8Lw@?PG%woSZXQjj6*Vt^Upn*J_6H+n~bW7=5z2wB#=p6YPBQTqO4`G}_JQ zl)g!IP{*f2S)xb5TbSn9HYfaKA~47$(}5Tp6vp8q|*qS=z$-I5MHR>GU8Tp=x;SkZx zCNlZe#9Qc!Rm-I8?U_2Dsn4*ccNaJb^x8>gW2ww`P;VFxo5jc#+d{oI&ySxz{cRJM z=W%WfsIyXY@_`(^w0LoGVc)WHc{r2S+4&`jUUxWz%=e3gePv?WULvLUEv%JVMS;yy z{pGd~VEAx6@2{k}nuMhIM~al+q*6iD~yVs=JAHC{KQxO6&$VaL`y zs~%LJL_|==g&s{NO`2T~x-&?46n+WA>-ipha&&akto`QDb>A%&j7dNs8BZy@@4lDu zcsXTR!cjJ_0Sr?h0&FF!e|?98Bld88Ql#G@5CNjA;#o8`#796QWLpQh9Kj?%jOm;n z|MBMqA9o%vIKgN%*pj#$O?S=Bfl^onb7fLk_~ttVfl$PEe!E4?=l=6#siDRLf?IF9 zGCV)muTW=+Q{H|^b2w9CzO(C*&*O6V7E3vJ00D!NoBJ;Oj_JaUkJ%buwOEbzR!UM9 z{yh`Z&;9*xM#jc(P*H2S^4%OlLgIm;cFJOk)ON%mRc{dhspY@-sI%Ha$DnFZq;S-# z>#lMs8Gy*@>&xitS1vKSIb-l3R&lYS$~m8Ohn4HvThB|J^C$B{z)ZeYV~@$e5u47AQjvC41G9Wu8T)}xEZi*z|vw#xU6-C0m!HP z)>pqwu48+y&+6sN9(HMp#VXVsw#)A09_oZWOiU9H0T~I2PBz&?eHWK5wkbA?+0uiZ zaPy`ldabh^b^X|kjIoBbapUuxpuoUj7Vq2mmk4vnZuRYN4m`}8us6rcP4U$`Mktwf zH48xYl$?_h^!+#g7^+Lbdyd3#`X;qlF89^^PA z(vMWl4a6}!zJ|4np{Uh%Q)*9(DY4f45sWs_SRcNFP4T76c=t&P=Il#Hdu{7499UQ=JRX22uYYD{>c1UH zq8|Wvfa%?B_4G&)(TTH{Js6Ox7f?rOztJ&%Wc!%V3u(nLHX1f+SX!ueK>?E3Anp{m zZ!7nddZf%hET#&S%e2^ORDQ#BqS0%!81JI`-XDk~@IAhTVPQ;sq2Q+1WzuWc z|4d1d^!Dca`}Z%%2`ijFZ7etpAi5Y(DD)#GrBaRASa+6t(3>EWsO5jx0-&G>f+u*W z(+|M%yI3(>yiSw?BkBebf`RnR{t1n$0InjDo(*P=+~0Z36zTEw>O|i8al!7z2pS67 z!wggqojp7{>CBYC{ah-g9(Or9d%Cx>K0lv-$B;v(2g}03f>PdJqSM53v$~Y_K_nPk z1_cS}Jv~3#cQ3CSqh0eXX0L1F>yxDcrwPHBse^2nqcpk^eH}Kd*#wuP+4Gq>eJnCE z`ECnvPqWLBLXDX$0vhQka6E8R=3}YsxCL#G#p)GuGBT(XU!(ILEU2p7z^hT`TWhe{ zhCPpMb0px_coJUMi~eWm0QBjB+kPD;jbkTkO{GMQo{-aa^gC=!rRfOnyLay-BxRc( ziBx3RZ59(8w$=>y%5{OErf=_CHK@Na-}t7(VyX(f7~}(9eSDUecwgb^V&8NTb7pF8 zyuE9bxg3DDPNNS?L5s3x*U{&@4s$)l5M28M*~nqXb!)ypDqEcNL+L_Y;N`x5i^=B$ zEP>bW8w^EQvV4L&L3)&}4p>r=#FCk|LJIF`X-BqP(I~_t*Zt!ghQ7zcpKpKIRV&kC zzOG}ABw!6z<6P|>&uF=QteJ2j0+D~4ulz8<|A5NaamfNyR2cRS#kCM-OuaPfK-$CJ{-mTOSw*Dk47?UdfuE5r-bwzneYQBBxRB zGT)I}rn>plY&`t)u6OG-x=(r>=W1Y9pPLI)+&e61I+_3TDK}tD|Jxs5ujJ|M5c>A* z_Y9_}XshtZT-+IRv~h#Qb5EaRiml_aFC7`7g$p+3i=E!&>>agrdxxkE5bx>?E;EJUfrMExb15jY2xVX7B3qXPqrLsVxtD%tLOD4jZwYY505KEid|NqF z6`mM?h%tFyE*B`CkmGEEO~Sh>=Iz}&Q^hcPxmq#LM8$e9HrZsDy`wpPw_~cqfQNAj zmIr1^CVPcNjn(=2740n{m;F!pHxaNP6t8C}*En-9O#fote9KLzCIjw6&4li*{obAn z{CQUhDS*lg##)}9Jd82;LN}-UTuGU>iY#EWhNY&b>m^N~yotzHSH8{D1t3saKkkS) zZMQdI;oP^bsvd7eh#edRnLIE3=w)>HywAEq*qU|;UBRn1z|C0U~4}ajD(!PkCB}mt(jtY2&LR(sO!n%-zsBqm`u+L$zU|He#nH^L?Mpd<&s?o zv&ZDeRj+5Cl3Z+QAe!fE))c_UfDh+m!5}|7-E5=5fdL?g(LvI%G`8C2< zkG&GW`5N+cZC#vjU99h;XEkxk|3aryQc}EGEG9J}%F4>2;gP))6B9q2`vR)GFixRQ zA6T)LQjC*-et-HaKBQgu4uBG?S9;&)j)egJv0gXVf3d4=XVl=;kNoD%#>{hcf%n}7 zZa%F*v+I#uN-{n!gT`Q*9kjyW9snPU>EDK3TL@_7yTDkCi;vO3teMB>_rxAc)3?5GeUABI_(^4UbgS-v&D} z&#Pv{w3FlFb;l+yPqrYpHLn=pXW~KLt1`63j%>N@)ZZ!cm(8wBuBvo zgX4KapW|w=X-3GG4&Q{RuqvQM4&!?^SMZ|bQJS`sbXt8nm&2>?+Z5tRsHk)r z3|w4XexB9ii1B`z7O8wZ_U~atLeQ(YWY25;)&yotp<$7uGWXYBorhd~;ZwHu&sRdN z4SZ+~ONVUF=pG&(-e=H<-u`|T^J`+u*<#?OSpm*KDLxUW5r@^+)tiyQl?Pt4y5jj3 zmjP{PdbB!rFA1+(H4Q^_yUzo5d!^1#L;RQ1eV4h=NqBn06KP>XkuazzC~^&&F5~wc zzltVQe=OADmYK?X4#2pWk}pYN_2K~Ni<&r1Qo0aOMJr1jWBcuot$`SAl7rS# zfe`R$MVjSZ%}r>;$(yDmb83b?GTeTEYXgJk{PxeU(Ja9j2Hn$LK=3y3sGY~fM-Y&} zD=NOl4t%H@V5+z6<8mH6H2Drn58(2HdIMbGGv1L0Qj<-~wKu0-d zET?~WF?N*FCU^KgRV2{h2#Ja|s$%XSpAY|5e9;Ji9un5jNS<*BZ&Q9QKcGf~Thq%#KHUmewWUJ;fUV>4D&;R|NEHpG_F!N>@L zZ{MEaz&rfu^hf3KxP7yp;kIgc7oIqp&b-pC(kE?cNwek+p$7mTa48`7M0Rg6F}v7h zQ(^1@i{@|bLfvR4e;OgX3JGPbdC;ic(0@Jrd6dX5&>npI9=H`nXnU@B z3=1^FZNAxs7NEy$w$sUXkoHfXKO=nyN<^7~5kX4A6o!fTN981F~|aLl)LG+M2Ybb4q%l;MH2F-1_r(! zPM{Xqlgn^FZ8Coupvf+W>-XEn))Zfjz|Zf^AiLYL_q~T2@R51FuNisq6&?eFgSAM6 zM0;tM`CJ9Lq%=r!Z-+-r{{GY`Q4Vz<_!lJd|2y|R{tx6FmbTqeqdS;^#sD)PMo{(rfJUO)Wud43@MgDil4qWBS)qoRr+{$@y8I_Wn>spJVpg88as^h4nA zN%q&TUuxpOI0L$Hi7Ed=&Hgtl1pd41&i}-{{!MoN{eJ%S#Lk+3Bm;t|gvc((s^JuV zs@`r*Zq9b4NTKST%hAkU5S~6TeYBG&Bbs#P0pa@hSSw~IddYU+;K*$j&%*;3{z{Jl z6oGhuQ+PmNjKs8CH}@Mb%K+E2u|-;ab8CxUV*hEJGZ=7U%>QI}_4X$f0!*VP7>$aP zb1o-gb!f-&{4kkWrAR9(&Txax?(dH*Kj^~$lGkW~RT7wA-vSXTj9NR%Zf~sSeZCv5 zHEM(PLbpxBiri48{V=Ga^K3)@@a7^#hr!yofBm->nz;s7G%h`p9Fd6eog?vB+7{}Y zJ_2phhxLKhL$~TVoL)pS&*bfGlNgINMt!-6-7(F2=Qqavt;4-xSa+KdjC4}_ZIPxU z_O;)_PL3AL$41h)1D1OOe=yeQ@-FMP{OBfkE!|!>CyAE6ZkQq?T ze60nAM^h63AwS`%+m?d*?Y9TjHE{3-s!gRepQd8y^s9bURdm=2Nr4jq#sTQJ6aa{T z2~9~ulS;}DrcUl~*!oIJ%JiL6k8C7Huon~W1}<0pd9J&6wXl*|Mp}C7pfGDA8l6N! zTDpO$=!V7`6^~IV@P6M# ze5GHn%3~>pmYRcFEW-?i7XSe={_YOy84FJiFs;FSZqVg4OaXs0eucCmF=fC^d6O6G1jEx$a>N#p_ zRN1MIPZ~Ucq4qaNOd2KLv!c7-emyq^cl1SQ1J&&~yspQU$N`{Go5&ylK5&)L8XCl0CkL3`j{y838hH z*}ex2#Hho(ak_-)R^ip@x?xx1vn`<$fFP_fZz`qceR;AJqStoXXwy;M$*f=g#` ze|hlnBPEONqx)&cvjB>cX9G%l!Ou(;HUKM?*Q-H1&j;!4`uje8 zCc<)BHJzNAI#_woYjy+oyUbO0b36T3%9rZ83_Y_4yc=ZF!os~Q5j;%A!)o(O z*F7Q;>So|=0_?+Pu|C)H$}*DVAyw>6Tb9jm;d}_{2fK0NQKvSHrzhs6`o5J8!FuX= zM!!zTS$NDTWN(7%!v}xIsvZ7<2AjD|@a{wpo9rog3Ph7wayFax%)9rCmipCHFNYU; zdBbWgk7N~K0?`3nK=j+U&oD0*hq~^{cVG#}4+|w{?Rsm&{>T(yFLAqmd-?~F5Ej4H z1~%*YKgN1M3gdPM)fv1RU^!FFywQVk<#opw zxieCQK_a!S00EuO{c)TZTWi~$8L2e0sygOd3 zi+Drkt6?vV+QW2qeqQy_&cXC}2{9}z8I4ebBwwRgw}nIB_YUX9dOABp5_q(K1sKrk z+^t9emH?vk;^}6@0~&+}I5)5uf|oyMs#?c?eh~3EETSp`PNkAdhv__U2CTp)z*GIU zc&r7u_OifDxtmUZ1_GA3gAv&27jr^R(;xit6xI-%r3O};MHNR!$M278BYQmJ=Tc^5 zEZB3(@TlM1u6nU%5d@-cRAx|a&j+$XV`2)m zt_~9-=;fQ{FZNo+Gq8=8I6f=BssYkz@z~N6g3rWgggRqaW7-O3o-QvgHK@k9B|oCd zU_+RY&ARu2%)2vN2rfm?E>mDX;{!c?lx!Nuc`sHE68anZ+f*L*XUoC=MDnasIdTE5 zhe}pn(kZV#k1w@>OwY z%bp0tV@SCUr~5b_C3ON2w@8^fH~;vt0EUrFi3qmaV!%g$3O!Ty8U2&@=m`8Dm7@KB z=PdvKv8DgLE3do2GqbYB12&{6K&4O$%EBXZ0j2aZb~2i;+z$^5hek$fnDX%)n<}FS z0&*?c(+xKOXQ2}yH%<(eX*_6!6=eN~>>%zuH>W0@Rvt4{Adiu2qQNz$UAw-=+F$vf z{TDHdHf6>Bw+1JRbC zEKZ1y+&pE_1uWu*&t+}^YXDYkjb{r5ghkQI9}Ip5j3YT)+w!2Wwx1(GPqE_m{Ye9w zET{u6bH=?->s6sATk`fQrGcz;#9ke~KLO8azQ#jW$ouS9h@_m{b+VcON0K?<+s+6+ z^&)~hCV@zm=n1%T{N}7d}nKGJrj;Yn*_O>lGjX_<;CF$ zW;GvwPtE*my~L-*)%j!*C#1*F&=RaPkMDclhZjl2nOV`E-d7+?G&C}{qv$gaQJBY% zCcZ;1+1%8qx27)fyN}(WRb#ronxmIF4mx&SZdj9B-4xq?Y9_7G(>?aN#~$V2u`ByQQM7)PCdkZ+HNYhi(ypYjG&;G%{J0nczeCB$K+%icQG`wb`?rthh~G7FB&t`8)at%Zei2_kl;+Kosx zcUOK%BAw9gh-8~ZgL+F-!|F}rzujVNcD_DcjHZC~`8Suk^TC1pjnNd= zs-=_Vg>#1PZi>d8Bo3gYpl35-^<7RB zTm3P6&4sEQ0}HftN`Z&>f@CH&}e1BjN!bY zct)r0%^nUPo_hJhbN$Xk^9?s{#cCx|KvZFtt>FH-p$br@!cr}q0pR_4 zm~_IR#Nm+b5%vI?!9Tx55J~+W-xRBs$T>Q)tTej-0mqGZ4nLZZJ-o8g{&21%Jh^(u z-DMO%W7nRv{9tNusSfLAtgNcR0)c_}c9&nN|g z8DJ~m>5NZI#LSl2ZMHw08UsWFM6G*nly*UV{Xt|fjTNy*Dp9bogYVUiaRAEdcSm34 z0`FmCkzgjbapK~1j^Ot8_RG_ih=CAvQ|P0I^MNBL0HCcwl5KN${Y+F;px^tSb8>T# zkdciBjbo}@j?@4v%*0R&?A*@GXWBnXuDFjs@$+=&GBJ5<((&YhF%zXm z5zZW=Mc#b8gGfq9WG@eu{L*tj|CB_p{S~Ocm}qJpGwU&fgzxM1X|xkR{Q0##mJzW~ z`D=i0>39L)d3^^*AyadV&)&U&_QYkV1)}V|@10{XD#0UuLST@7;vfpnA-wCNgxMW)yQZx7wg?bJl5d2>V9nCpM=Jwro7zMEs# zQvHt1>1maJghMFbaCbdjE~Q3o?eq%k*1QwP1a4gbaZVp_+vZMjnGkMx)UH0vIq z^gJMDgq%&mF_d!a1+!%YT=s8^sHp+_apIr*kt*H5dc~?0U8lOdz;vjiDNt|ugO5&f z&qb?VW;DDl85knYWYmk)es`_ydNO@iSqSWI)?q*=G04cs zP_VpGrd6!hydxGGF&kIr0qf29uh9jE?f2~Wkx@hrY%RERwMEJ7mImNy0?27g+pbEb zk>cBdNROx?<$|A%m!Efxj1Lxa5i$5Lv*(V*F2jE$Ck59yZ)?WulKFF}Q>WvWcCQ_aVxx|^ef?*6yyH4_3`JD3y; zfU8ZP%4xnSjul3yd+#WIr6oqt;{n;5s@dZw=8uas#0T-_%mEfh3<+;PR!=aq)hwt{ zhk=mYdeEvO>KCvC8#nJtE+}fF^$v0{Q^Gk@qFyKsYVp|bLR3YcFnGKsy7uxkv|d4m zg(gQ4Zry{wjZU4e8$8@%Su?hW#wG=~I8o_r^?T^(SbnOY3+;)I6t z@UYOi|4eu;o==uNRFX`)2hPc|bD9FbOXU8DZVIZvNzKRNn%`DRZAw`D7J=NvXt9}O z#gK2tk*cO)iH7C^s>nFyqN26Rt#iZgBe=&$m9kbAY%)`P5gya<_CW67fWYz|S2X=h#gaxgc;g@%8{HK~M{*Hz|(*IDI@!6$fCbhU?T66cF;$tBHl z7eIEvt07NDOcgH4Ib_VwW76zsH=nrm1ykZxQmjhaOIs75?5Gv)NnzxrJbq z)&7(a8|$!Q)#3)8g~S*BHW`ZR)7g0?Ake&6UsnDIVbxt|5r;>EiVL)PK0SD{P{9>C`A&1R!cCB(Dn{W4PpwrIR$C`EEKZ=}*)W*DhiPaOPR-^lWhsTFiT-6HV z>g{SWwt0Y3lRx}7Y>Gk1IJJ&_j(t9lRVR5wG={wEd${y^rOWBWeWK!4Rz^7vCjb;2 z&iQ@cecZxv0gzQXXk6`2PG3yNAQ%14o^+k%)3K4iGlRBzy{&54pqc9{-TfAD`_!gb z1lx1noONNNQat6&Z^?KsWU$+}HN7+ryK1~`VW2oj39#o~fWH9Y10*JfCnnU(vi$6} z)QOnve?p=2#*1xU4JX*@JYF2i0In$j)p#~QQ$j-io|nh@RmKA}Jk0>kpd@kc5{SkO z})yN%+^{k?ziub;b3AK{u<}^Zgy53qEX#$bIrIg?|1~G`1%g}?cm@5 znL5VtAb>y=hlYCDm_MpFdR`UJ+RbM_JiB%PS1wknD?XfUk;NGXsdu#d*z7fq14UOZ z+k=>Z;+}L3x5_WfnoJF3Pi1y0Xum(CUE(gU0b=;cN{c#VcVceL7stiiUBu`9ps>Ei zc90p)VY64#lZW!FYKcnIuGTQ;+P4|15^N&gW#N>TZART3+O@p>!zaIZ`4P3rzKHsG z7#wWuZ|m7Yae0P4!CK`Gg=9*~iM2jyKph$m38eOfSRGJPQ+kQsSlyQ* zSV(fNUIY+o4?a>=lsD|8b~E>|Ws;%l-rhepH`OdgllPD->U`IQ+B%%WQh9H&zNmS4 zN$cqeLa{2p4Nn&>*4wmj*lIBP`S}5FXzyw2rWOPO?Ysjrf)P`Ka~0~;YknPW3<4j7 zG*dKF`R+U-Oix`NPSBjsH^nme(~6`LUjeUPSmIk~G?`#I;8h(Jq}jfV!;1@sVi^MS z_QEMlIES-rA;QP05&5{w!;%V1NYX^z@l$?W-qt`gp}u}we^wnZ$JnjsD>ZmY=vkFl zO9WNBbffZ0)OYx;l8|4)ss3rKI$?SA_n4eyvspx0taS*mZ9bsXeLSGY04O7By1oT~ zuRIPHd51G)CCY`9?4Fn3_|H1wycO0tLt{AC_O8x)PG&9{W!WDW%;|BOCeF78>Yh7c zXNq*Sf#f%#pzn{<#x3;z4ZE{BvoY+qZ?l0ZWZ`~n-}Z3Y((GCHz`wx_c?1W zh4sSsO4Wme=i`dXSNNz?CEv%wcZ?%FIV6&j!y_Zg@dbX&q|cO}G=ahF`4u=L<(&4JBS2)QBUr(_NaP0}#7wq#xwNrrm{}%oJD<1tH0LcHf zOaBu@&bU89ap|#DxEs$njJ74DgHF#jFAo5?x=&rYMY?A6uWw)H)XShRrO+(J2 zwA4VgAm^9?5U->9%KKvbnk*jY<2(RHtf`uWeSOKfMcejl+@jRuy}O2?|!53&o^(3Gvd2 zKi5+bKo}4)IVWwealv5fcAWp|`RE%ow&gF3-!~&CUnKd;O8KZfm59Ksn_M zP*Ec81IN44JRvZZ%sgQMZS}an9>j!)SDjRr7ck`=Ip?rm{vA4Kc7MCoXjKl>*UCTL z-^`VQ%nnBw-z6sB0-)g4C7fS_;}Q&1rW5jeMv_|2$!TgX;2tjj`px>dcYhUbNt&O* z8^;E?%xye&nMrh-mcJ$!!g+cKXG*LtwukQT{|LQIEzg^_o69c55qcFp{WnXnf(_sP zuV$`19O``!pRG<=@^rF9p-ziE35OY>MG2k3FpW@_Y#EF&2&LsnizPXT43V+S7^4w` z(9?oa#7AeBSpf^d6rW)7*=6 zb=$Hf83)g%l$6vx8jVImMhWNU^bS&-xZIfG1f}Y*aVa=E%&_d(?WWNVlEo3mK4atK zq>{Wi^cD?NS9X=Zf^*qD-a+j9%FEY(-nC0XS$U@oL>F_99HmSbwLWsYc3qE+U^A?n zF!`Luj0X=W33n8g*&K)mREG?@!hvV)PF6vnAMf%u4jFLSR+fV4QC5_f*Iah5`H2yN z@$oSPu*dc&Z&ER>6tF>>_keIJfH2<(Dx#r~x!=gh85V&0xUY1yQ~cf3)KmyuhG@(p zw~J&7TSq$~lW4r0(KjPBsG1RpGP1MFOon3I@Qrgd2h~zZwoy6anV>;dY1(RklI4c@ zJ5RTnWHK08$5XbGs{A_wyF?unu!@sj4t=co5aNXHij-U_V1d1LpVjlJUspYbKFEgG zPEMYk;xs;JXrzgEEkQN_au{C2Vu8R6CW{40N!`ualhMbB1^M~;tHL%Nvg4y02?rK5 zr)1)p>6yjiWw}ZG!W#8{+rVfRQ}FTmb#1qbhE){>lA66a3AllL_;4~X448>(aJl!l zVyNix9NgEhUt=!z>XJ%IbO1KC>_zoht5-NjN7p8xqu#uE(}|-!tB|vB@iS{Ctcb2qdpG4+Jyr=abNYUHY&{QUfe<;L#qQ59LIPW$9q1ULKy zq4NZF^^Ox*PtPk<-Ic?>zP@GWT2V10!d8~4?gI3FefSrKe0bwX!Q=7JGk6=B zgIl-$2Tcu)62awp`;NBt>u28mavSUCSL|97NJSC?aTD*9Yr@?r5NQt_BxfwmexWw= zb!&)DuQz{huyf~OwNz`?ks}Ju2$xZflReNkVv*;*XN#AXyyh}mV zo2;(jF41GEU+`tHU>;y#GTOXJz2bncEzbI+#${s!MW^s1_A zpEqb<|9mGf8hJ)oVG-#(ubvRtN3K55k;i(g-$5si1;U?kfT-cf$jI-xx#e0;CE9zD zEs=_=o{-`t(d5*7KWF&^)3vLQ7XG~Ulr*I2)vmH<@8tq;I;dwGT}2)bXmHcKXKIh# zHu+A~H1v>uN6hK-B`7UZQx`e&o>PxsCVpUsHRR@Q2V3YQ>@rqO9I=JU1q-yq?5i#I zQ{cUUGnUCY-EaaP=O@Go6$AqD2mT&8(gUYYb~%)!{o}emW>_^e76QKHB2-KRD(Q4O zY`JEgymd3qOgesletZ)KW>F5Hp`_HMH*V}q!=rUi)xBk^npK12NVEt)4$Fed0iT=e zXZd(D%3XGuk^NQP28Ku^sv-i23Die#X59y6`*HjKjoAGww*GhFK6K#FyY_aBvuL@e z&W;VB#Hq42kDWuih}SyDw48K%5FUR*1?XG~~3g@z87lj_@V)kzr7GF;y5VSPQ{cxIcEDaz7@v$hA0k zBaq{2cR1tvoJ16Z_LWXxMm;iY2pzr#-L=BIcO4|UIOy|+Gfi=QXp-Uhr%%((+-?Gn z+E1QTe|Yot3En_m_hQb!GX+vV4iU0rp8LUGSy`EVuqp0qn7Qy(`t78f4l5-TJz;pBWrrgZ5%+p(yI;_hfgGV z?~7>iK)zhyxD}#^YL22(&EP)zX%NQR6Mrz1EWKC{;E)N7A6*%zfTUmwtu{)Nn1F8; zE{~6dPrPHnlHCKb|A_G!U)eGJST;a{hJ9Wk_13Na+6&if@XP22jG)eUS!d}ET=%}d zKG377oVf6-k-LMjp#r^_&-_6t8vj3Y2k*yk9ODahT^n5L5Qy8iQhGsKy=d(Qa(^)L zH_3qz`>XX7kstmQna_lqo0wqj=OzmnoERr~fa#LU7;z&z+h4)Gjt+sg-~y&9aEx@d z)b%pkOJ7m*DUc}1lp@kf4RvAeJxpVlpP}eCt)LU1SvU{eN&)Ep(qHWsHAu)(F$&mW z6El`(ZNyvmX~i@0b|EuVd_#7sm&Yf=ymj~a*B=k(m~t;B-%PGfn>qm!u4maUOP&+I z#NBGX=(%lteEbcYjm0;8&4f)5sStIwyLUW!d)fQVk5x>V$~3a+Uv&66ze|gq)-yt( zBkY-o8SOA;6&$s`Bx>JmM`9|d4%!VqjY?-M%&k>{tU63}t#KBWmU{0CPX$^5-93M1 zE(_c}W2l{-y;pw~{w!b>d|Bs9p4n^59S(Y~PhhT1Pq6i0Ch^DD456wit9I}Nq(PgV zC)AD+y(qoZCy++eJ5%qR_r$0@+YAe9Z>0k_=ooQ0n8E7S$d4<8^9Yr-h}D&Kq~c=N z(umZK-b=+`28>Tn>)0UU7AqwYh-mYJD=1QlzviydCseEJ=hlzBx|Kc~aWaW~sw}L% z8m9JT7e=2xSxU$BKjZ7L%1=9)$ILr}J_Ir#`HoZN?%ey&vTG$&cXkO!)7rh;iZXJl zj1%l-YuoPyQ~=IpN%5~a0o}#Pw&GO}GUjO}GK$-_FvG7T;0ovYmJh!bFH77fzqGVK zb_Fr-Qe-W>K1xcGA#l4uJyT8+k(kxhZ|lYLX3nHQ<3`gNx z7KQ44lb+|jk3hSXp2s%qkkML^y*b2$dm+`5bEad{w&LG@i#A`H+$ABNJY8&E1ebmd zzp+>E`EzZLriL39q9h5=0mAwo6;52pYNY+B9_Ik|0)se$lvZpggAAIgX{E=|+O%0h zV&Zf`Vn3iy#zWhTCo9)|kr@+?$CtE>af)S-66ksUr{~-~-5L!9z}bJmgX?_e9SoG zCQQ9|G>iJu0~zUzw6ub3{l=o#uTNmgT{^?+V#g+OyGadq5*T#fw^JfLw;qTr!I6O8 z-*idXoCOk{1#-~HpXpv*yLK6rcvm$}2{^(TD891FW+Q&hJAhd*##dbVhI4em4R%Ov z1EMStgnrPfkXKYJma|+rp{1n-K2Z`U`rK8}?SKXTX6?ojil381hMiKUpZ^lfs`RRb zND9vE?S&hgREweGtgWkiQc3@8PxBq6jQo6ko2kC4LQ52HdaiKtS1|VLTwEBcb=~Go3^$tfD7Ze=yL6W&)Dc#extf& z-vA_A+KGnNP@wG{1UD6&&VW}NKE+85OgrZ!MkTIU3K=NiJ43G&IwT(c!^Qudq5pF2 ZyZTRvi+w&c9&nv+Y>uI::}{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{::}{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 +*/ diff --git a/examples/corelib/time/calendarbackendplugin/plugin/CMakeLists.txt b/examples/corelib/time/calendarbackendplugin/plugin/CMakeLists.txt new file mode 100644 index 00000000000..bac4118b8f5 --- /dev/null +++ b/examples/corelib/time/calendarbackendplugin/plugin/CMakeLists.txt @@ -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 +) diff --git a/examples/corelib/time/calendarbackendplugin/plugin/calendarbackend.cpp b/examples/corelib/time/calendarbackendplugin/plugin/calendarbackend.cpp new file mode 100644 index 00000000000..fbf95349321 --- /dev/null +++ b/examples/corelib/time/calendarbackendplugin/plugin/calendarbackend.cpp @@ -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 + +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] diff --git a/examples/corelib/time/calendarbackendplugin/plugin/calendarbackend.h b/examples/corelib/time/calendarbackendplugin/plugin/calendarbackend.h new file mode 100644 index 00000000000..5b565c794c3 --- /dev/null +++ b/examples/corelib/time/calendarbackendplugin/plugin/calendarbackend.h @@ -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 +//![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 diff --git a/examples/corelib/time/calendarbackendplugin/plugin/calendarplugin.cpp b/examples/corelib/time/calendarbackendplugin/plugin/calendarplugin.cpp new file mode 100644 index 00000000000..462493bf0dd --- /dev/null +++ b/examples/corelib/time/calendarbackendplugin/plugin/calendarplugin.cpp @@ -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] diff --git a/examples/corelib/time/calendarbackendplugin/plugin/calendarplugin.h b/examples/corelib/time/calendarbackendplugin/plugin/calendarplugin.h new file mode 100644 index 00000000000..0d5c9cf2158 --- /dev/null +++ b/examples/corelib/time/calendarbackendplugin/plugin/calendarplugin.h @@ -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 +//![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 diff --git a/examples/corelib/time/calendarbackendplugin/plugin/plugin.pro b/examples/corelib/time/calendarbackendplugin/plugin/plugin.pro new file mode 100644 index 00000000000..6a5f814e858 --- /dev/null +++ b/examples/corelib/time/calendarbackendplugin/plugin/plugin.pro @@ -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 diff --git a/examples/corelib/time/time.pro b/examples/corelib/time/time.pro new file mode 100644 index 00000000000..4d24416e345 --- /dev/null +++ b/examples/corelib/time/time.pro @@ -0,0 +1,4 @@ +TEMPLATE = subdirs +CONFIG += no_docs_target + +SUBDIRS = calendarbackendplugin