From 8a64b6acea861313e0f28b7224fdf20848fbbceb Mon Sep 17 00:00:00 2001 From: Vadim Zeitlin Date: Wed, 4 Nov 2020 22:59:11 +0100 Subject: [PATCH 1/4] Fix inheritance hierarchy of wxTimePickerCtrlGeneric This class must not derive from the native wxDatePickerCtrl, as it doesn't make much sense and resulted in the need for an ugly hack with either overriding unused and inapplicable pure virtual methods in this class itself, as was originally done in 569c7d8ccb (Add wxTimePickerCtrl class., 2011-09-29) when it was introduced, or not making these methods pure virtual in the first place, as was done in d0da5061ce (Dirty hack to allow generic wxDatePickerCtrl to compile under MSW., 2011-10-20), but didn't really fix the problem. Do fix it now by using different hierarchies for the native and generic classes. The main disadvantage of doing it is that there is no common base class for wxTimePickerCtrl and wxTimePickerCtrlGeneric providing SetTime() and GetTime() methods any more, but this seems like a relatively small price to pay because real applications won't be using these two classes simultaneously, as the calendar sample does, anyhow. --- include/wx/generic/timectrl.h | 6 ++++-- include/wx/msw/datetimectrl.h | 28 +++------------------------- include/wx/timectrl.h | 13 ++++++++++--- samples/calendar/calendar.cpp | 35 +++++++++++++++++++++++++++++------ 4 files changed, 46 insertions(+), 36 deletions(-) diff --git a/include/wx/generic/timectrl.h b/include/wx/generic/timectrl.h index eea7daa2df..84f7a4f261 100644 --- a/include/wx/generic/timectrl.h +++ b/include/wx/generic/timectrl.h @@ -13,11 +13,13 @@ #include "wx/containr.h" #include "wx/compositewin.h" +typedef wxTimePickerCtrlCommonBase wxTimePickerCtrlGenericBase; + class WXDLLIMPEXP_ADV wxTimePickerCtrlGeneric - : public wxCompositeWindow< wxNavigationEnabled > + : public wxCompositeWindow< wxNavigationEnabled > { public: - typedef wxCompositeWindow< wxNavigationEnabled > Base; + typedef wxCompositeWindow< wxNavigationEnabled > Base; // Creating the control. wxTimePickerCtrlGeneric() { Init(); } diff --git a/include/wx/msw/datetimectrl.h b/include/wx/msw/datetimectrl.h index 97afc1f40c..cc6eab89b7 100644 --- a/include/wx/msw/datetimectrl.h +++ b/include/wx/msw/datetimectrl.h @@ -46,39 +46,17 @@ protected: const wxValidator& validator, const wxString& name); - // Notice that the methods below must be overridden in all native MSW - // classes inheriting from this one but they can't be pure virtual because - // the generic implementations, not needing nor able to implement them, is - // also derived from this class currently. The real problem is, of course, - // this wrong class structure because the generic classes also inherit the - // wrong implementations of Set/GetValue() and DoGetBestSize() but as they - // override these methods anyhow, it does work -- but is definitely ugly - // and need to be changed (but how?) in the future. - #if wxUSE_INTL // Override to return the date/time format used by this control. - virtual wxLocaleInfo MSWGetFormat() const /* = 0 */ - { - wxFAIL_MSG( "Unreachable" ); - return wxLOCALE_TIME_FMT; - } + virtual wxLocaleInfo MSWGetFormat() const = 0; #endif // wxUSE_INTL // Override to indicate whether we can have no date at all. - virtual bool MSWAllowsNone() const /* = 0 */ - { - wxFAIL_MSG( "Unreachable" ); - return false; - } + virtual bool MSWAllowsNone() const = 0; // Override to update m_date and send the event when the control contents // changes, return true if the event was handled. - virtual bool MSWOnDateTimeChange(const tagNMDATETIMECHANGE& dtch) /* = 0 */ - { - wxUnusedVar(dtch); - wxFAIL_MSG( "Unreachable" ); - return false; - } + virtual bool MSWOnDateTimeChange(const tagNMDATETIMECHANGE& dtch) = 0; // the date currently shown by the control, may be invalid diff --git a/include/wx/timectrl.h b/include/wx/timectrl.h index 928d9aedf3..4e4a7a3758 100644 --- a/include/wx/timectrl.h +++ b/include/wx/timectrl.h @@ -29,7 +29,10 @@ enum // wxTimePickerCtrl: Allow the user to enter the time. // ---------------------------------------------------------------------------- -class WXDLLIMPEXP_ADV wxTimePickerCtrlBase : public wxDateTimePickerCtrl +// The template argument must be a class deriving from wxDateTimePickerCtrlBase +// (i.e. in practice either this class itself or wxDateTimePickerCtrl). +template +class wxTimePickerCtrlCommonBase : public Base { public: /* @@ -67,7 +70,7 @@ public: return false; } - SetValue(dt); + this->SetValue(dt); return true; } @@ -78,7 +81,7 @@ public: wxCHECK_MSG( hour && min && sec, false, wxS("Time component pointers must be non-NULL") ); - const wxDateTime::Tm tm = GetValue().GetTm(); + const wxDateTime::Tm tm = this->GetValue().GetTm(); *hour = tm.hour; *min = tm.min; *sec = tm.sec; @@ -87,6 +90,10 @@ public: } }; +// This class is defined mostly for compatibility and is used as the base class +// by native wxTimePickerCtrl implementations. +typedef wxTimePickerCtrlCommonBase wxTimePickerCtrlBase; + #if defined(__WXMSW__) && !defined(__WXUNIVERSAL__) #include "wx/msw/timectrl.h" diff --git a/samples/calendar/calendar.cpp b/samples/calendar/calendar.cpp index df95946a7d..28964de1f0 100644 --- a/samples/calendar/calendar.cpp +++ b/samples/calendar/calendar.cpp @@ -218,12 +218,24 @@ class MyTimeDialog : public wxDialog public: MyTimeDialog(wxWindow* parent); - wxDateTime GetTime() const { return m_timePicker->GetValue(); } + wxDateTime GetTime() const + { +#if wxUSE_TIMEPICKCTRL_GENERIC + if ( m_timePickerGeneric ) + return m_timePickerGeneric->GetValue(); +#endif // wxUSE_TIMEPICKCTRL_GENERIC + + return m_timePicker->GetValue(); + } private: void OnTimeChange(wxDateEvent& event); - wxTimePickerCtrlBase* m_timePicker; + wxTimePickerCtrl* m_timePicker; +#if wxUSE_TIMEPICKCTRL_GENERIC + wxTimePickerCtrlGeneric* m_timePickerGeneric; +#endif // wxUSE_TIMEPICKCTRL_GENERIC + wxStaticText* m_timeText; wxDECLARE_EVENT_TABLE(); @@ -987,20 +999,31 @@ wxEND_EVENT_TABLE() MyTimeDialog::MyTimeDialog(wxWindow *parent) : wxDialog(parent, wxID_ANY, wxString("Calendar: Choose time")) { + wxWindow* timePickerWindow = NULL; + #if wxUSE_TIMEPICKCTRL_GENERIC + m_timePickerGeneric = NULL; + m_timePicker = NULL; + wxFrame *frame = (wxFrame *)wxGetTopLevelParent(parent); if ( frame && frame->GetMenuBar()->IsChecked(Calendar_TimePicker_Generic) ) - m_timePicker = new wxTimePickerCtrlGeneric(this, wxID_ANY); + { + m_timePickerGeneric = new wxTimePickerCtrlGeneric(this, wxID_ANY); + timePickerWindow = m_timePickerGeneric; + } else #endif // wxUSE_TIMEPICKCTRL_GENERIC m_timePicker = new wxTimePickerCtrl(this, wxID_ANY); - m_timeText = new wxStaticText(this, wxID_ANY, - m_timePicker->GetValue().FormatISOTime()); + + if ( !timePickerWindow ) + timePickerWindow = m_timePicker; + + m_timeText = new wxStaticText(this, wxID_ANY, GetTime().FormatISOTime()); const wxSizerFlags flags = wxSizerFlags().Centre().Border(); wxFlexGridSizer* const sizerMain = new wxFlexGridSizer(2); sizerMain->Add(new wxStaticText(this, wxID_ANY, "Enter &time:"), flags); - sizerMain->Add(m_timePicker, flags); + sizerMain->Add(timePickerWindow, flags); sizerMain->Add(new wxStaticText(this, wxID_ANY, "Time in ISO format:"), flags); From 6d4ce92c204cd2431aea3fe3a1b417153ed5961c Mon Sep 17 00:00:00 2001 From: Vadim Zeitlin Date: Wed, 4 Nov 2020 23:27:44 +0100 Subject: [PATCH 2/4] Fix assert failures with wxDP_ALLOWNONE in the widgets sample DatePickerWidgetsPage::OnDateChanged() must check whether the wxDateTime value received from the control or event is valid before formatting it, as this may not be the case when wxDP_ALLOWNONE is on, and trying to format an invalid date results in assertion failures (and random junk in the output). --- samples/widgets/datepick.cpp | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/samples/widgets/datepick.cpp b/samples/widgets/datepick.cpp index 91b8392cdb..3bc561b472 100644 --- a/samples/widgets/datepick.cpp +++ b/samples/widgets/datepick.cpp @@ -327,11 +327,19 @@ void DatePickerWidgetsPage::OnButtonSetRange(wxCommandEvent& WXUNUSED(event)) } } +// Helper function which has to be used here because the controls with +// wxDP_ALLOWNONE style can have invalid date, both in the control itself and +// in the event it generates. +static wxString FormatPossiblyInvalidDate(const wxDateTime& dt) +{ + return dt.IsValid() ? dt.FormatISOCombined() : wxString("[none]"); +} + void DatePickerWidgetsPage::OnDateChanged(wxDateEvent& event) { wxLogMessage("Date changed, now is %s (control value is %s).", - event.GetDate().FormatISOCombined(), - m_datePicker->GetValue().FormatISOCombined()); + FormatPossiblyInvalidDate(event.GetDate()), + FormatPossiblyInvalidDate(m_datePicker->GetValue())); } #endif // wxUSE_DATEPICKCTRL From 02f1ee398747b824904361669f6723972d68eb7e Mon Sep 17 00:00:00 2001 From: Vadim Zeitlin Date: Thu, 5 Nov 2020 01:21:26 +0100 Subject: [PATCH 3/4] Add wxDateTimePickerCtrl::SetNullText() This allows to customize the string shown when there is no valid date under MSW (only, for now) and can be notably used to suppress the unused date completely, which can be useful to lighten up the display when there are many controls. Add UI elements to the widgets sample allowing to test the new function. --- include/wx/datetimectrl.h | 7 ++++ include/wx/msw/datetimectrl.h | 15 +++++++++ interface/wx/datectrl.h | 16 ++++++++++ samples/widgets/datepick.cpp | 23 ++++++++++++++ src/msw/datetimectrl.cpp | 60 ++++++++++++++++++++++++++++++++++- 5 files changed, 120 insertions(+), 1 deletion(-) diff --git a/include/wx/datetimectrl.h b/include/wx/datetimectrl.h index 30f23dfffe..29cff3c7c9 100644 --- a/include/wx/datetimectrl.h +++ b/include/wx/datetimectrl.h @@ -32,6 +32,13 @@ public: // Set/get the date or time (in the latter case, time part is ignored). virtual void SetValue(const wxDateTime& dt) = 0; virtual wxDateTime GetValue() const = 0; + + // For the controls with wxDP_ALLOWNONE style, set the string displayed + // when the control doesn't have any valid value. Currently this is only + // actually used under MSW, where it can be used to override the previous + // value which is still displayed by the control in this case, and ignored + // elsewhere. + virtual void SetNullText(const wxString& WXUNUSED(text)) { } }; #if defined(__WXMSW__) && !defined(__WXUNIVERSAL__) diff --git a/include/wx/msw/datetimectrl.h b/include/wx/msw/datetimectrl.h index cc6eab89b7..a56c87d580 100644 --- a/include/wx/msw/datetimectrl.h +++ b/include/wx/msw/datetimectrl.h @@ -26,6 +26,8 @@ public: virtual void SetValue(const wxDateTime& dt) wxOVERRIDE; virtual wxDateTime GetValue() const wxOVERRIDE; + virtual void SetNullText(const wxString& text) wxOVERRIDE; + // returns true if the platform should explicitly apply a theme border virtual bool CanApplyThemeBorder() const wxOVERRIDE { return false; } @@ -61,6 +63,19 @@ protected: // the date currently shown by the control, may be invalid wxDateTime m_date; + +private: + // Helper setting the appropriate format depending on the passed in state. + void MSWUpdateFormat(bool valid); + + // Same thing, but only doing if the validity differs from the date + // validity, i.e. avoiding useless work if nothing needs to be done. + void MSWUpdateFormatIfNeeded(bool valid); + + + // shown when there is no valid value (so only used with wxDP_ALLOWNONE), + // always non-empty if SetNullText() was called, see the comments there + wxString m_nullText; }; #endif // _WX_MSW_DATETIMECTRL_H_ diff --git a/interface/wx/datectrl.h b/interface/wx/datectrl.h index 72199583e7..59d1eda289 100644 --- a/interface/wx/datectrl.h +++ b/interface/wx/datectrl.h @@ -168,6 +168,22 @@ public: */ virtual wxDateTime GetValue() const; + /** + Set the text to show when there is no valid value. + + For the controls with @c wxDP_ALLOWNONE style, set the string displayed + when the control doesn't have any valid value. Currently this is only + actually used under MSW, where it can be used to override the previous + value which is still displayed by the control in this case, and ignored + elsewhere. + + Notably, @a text can be empty to completely hide the date if no valid + date is specified. + + @since 3.1.5 + */ + void SetNullText(const wxString& text); + /** Sets the valid range for the date selection. If @a dt1 is valid, it becomes the earliest date (inclusive) accepted by the control. If diff --git a/samples/widgets/datepick.cpp b/samples/widgets/datepick.cpp index 3bc561b472..f0374900da 100644 --- a/samples/widgets/datepick.cpp +++ b/samples/widgets/datepick.cpp @@ -54,6 +54,7 @@ enum DatePickerPage_Reset = wxID_HIGHEST, DatePickerPage_Set, DatePickerPage_SetRange, + DatePickerPage_SetNullText, DatePickerPage_Picker }; @@ -78,6 +79,7 @@ protected: void OnButtonSet(wxCommandEvent& event); void OnButtonSetRange(wxCommandEvent& event); + void OnButtonSetNullText(wxCommandEvent& event); void OnButtonReset(wxCommandEvent& event); // reset the date picker parameters @@ -96,6 +98,7 @@ protected: wxTextCtrl *m_textCur; wxTextCtrl *m_textMin; wxTextCtrl *m_textMax; + wxTextCtrl *m_textNull; wxRadioBox* m_radioKind; wxCheckBox* m_chkStyleCentury; @@ -117,6 +120,7 @@ wxBEGIN_EVENT_TABLE(DatePickerWidgetsPage, WidgetsPage) EVT_BUTTON(DatePickerPage_Reset, DatePickerWidgetsPage::OnButtonReset) EVT_BUTTON(DatePickerPage_Set, DatePickerWidgetsPage::OnButtonSet) EVT_BUTTON(DatePickerPage_SetRange, DatePickerWidgetsPage::OnButtonSetRange) + EVT_BUTTON(DatePickerPage_SetNullText, DatePickerWidgetsPage::OnButtonSetNullText) EVT_DATE_CHANGED(wxID_ANY, DatePickerWidgetsPage::OnDateChanged) wxEND_EVENT_TABLE() @@ -197,6 +201,20 @@ void DatePickerWidgetsPage::CreateContent() sizerMiddle->Add(new wxButton(this, DatePickerPage_SetRange, "Set &range"), wxSizerFlags().Centre().Border()); + sizerMiddle->AddSpacer(10); + + sizerMiddle->Add(CreateSizerWithTextAndLabel + ( + "&Null text", + wxID_ANY, + &m_textNull + ), + wxSizerFlags().Expand().Border()); + + sizerMiddle->Add(new wxButton(this, DatePickerPage_SetNullText, + "Set &null text"), + wxSizerFlags().Centre().Border()); + // right pane: control itself wxSizer *sizerRight = new wxBoxSizer(wxHORIZONTAL); @@ -327,6 +345,11 @@ void DatePickerWidgetsPage::OnButtonSetRange(wxCommandEvent& WXUNUSED(event)) } } +void DatePickerWidgetsPage::OnButtonSetNullText(wxCommandEvent& WXUNUSED(event)) +{ + m_datePicker->SetNullText(m_textNull->GetValue()); +} + // Helper function which has to be used here because the controls with // wxDP_ALLOWNONE style can have invalid date, both in the control itself and // in the event it generates. diff --git a/src/msw/datetimectrl.cpp b/src/msw/datetimectrl.cpp index df023a5231..41059fa360 100644 --- a/src/msw/datetimectrl.cpp +++ b/src/msw/datetimectrl.cpp @@ -99,6 +99,8 @@ void wxDateTimePickerCtrl::SetValue(const wxDateTime& dt) return; } + MSWUpdateFormatIfNeeded(dt.IsValid()); + m_date = dt; } @@ -107,6 +109,57 @@ wxDateTime wxDateTimePickerCtrl::GetValue() const return m_date; } +void wxDateTimePickerCtrl::MSWUpdateFormatIfNeeded(bool valid) +{ + if ( MSWAllowsNone() && !m_nullText.empty() && valid != m_date.IsValid() ) + MSWUpdateFormat(valid); +} + +void wxDateTimePickerCtrl::MSWUpdateFormat(bool valid) +{ + // We just use NULL to reset to the default format when the date is valid, + // as the control seems to remember whichever format was used when it was + // created, i.e. this works both with and without wxDP_SHOWCENTURY. + + // Note: due to a bug in MinGW headers, with missing parentheses around the + // macro argument (corrected in or before 8.2, but still existing in 5.3), + // we have to use a temporary variable here. + const TCHAR* const format = valid ? NULL : m_nullText.t_str(); + DateTime_SetFormat(GetHwnd(), format); +} + +void wxDateTimePickerCtrl::SetNullText(const wxString& text) +{ + m_nullText = text; + if ( m_nullText.empty() ) + { + // Using empty format doesn't work with the native control, it just + // uses the default short date format in this case, so set the format + // to the single space which is more or less guaranteed to work as it's + // the semi-official way to clear the control contents when it doesn't + // have any valid value, according to Microsoft's old KB document + // Q238077, which can still be found online by searching for its + // number, even if it's not available on Microsoft web site any more. + // + // Coincidentally, it's also convenient for us, as we can just check if + // null text is empty to see if we need to use it elsewhere in the code. + m_nullText = wxS(" "); + } + else + { + // We need to quote the text, as otherwise format specifiers (e.g. + // "d", "m" etc) would be interpreted specially by the control. To make + // things simple, we just quote it entirely and always. + m_nullText.Replace("'", "''"); + m_nullText.insert(0, "'"); + m_nullText.append("'"); + } + + // Apply it immediately if we don't have any value right now. + if ( !m_date.IsValid() ) + MSWUpdateFormat(false); +} + wxSize wxDateTimePickerCtrl::DoGetBestSize() const { wxClientDC dc(const_cast(this)); @@ -168,7 +221,12 @@ wxDateTimePickerCtrl::MSWOnNotify(int idCtrl, WXLPARAM lParam, WXLPARAM *result) switch ( hdr->code ) { case DTN_DATETIMECHANGE: - if ( MSWOnDateTimeChange(*(NMDATETIMECHANGE*)(hdr)) ) + const NMDATETIMECHANGE& dtch = *(NMDATETIMECHANGE*)(hdr); + + // Update the format before showing the new date if necessary. + MSWUpdateFormatIfNeeded(dtch.dwFlags == GDT_VALID); + + if ( MSWOnDateTimeChange(dtch) ) { *result = 0; return true; From 9bc6c3c2347453e6255bf2e4d10f95f4c243934d Mon Sep 17 00:00:00 2001 From: Vadim Zeitlin Date: Thu, 5 Nov 2020 01:39:20 +0100 Subject: [PATCH 4/4] Add support for wxDatePickerCtrl::SetNullText() to XRC Recognize null-text element. --- misc/schema/xrc_schema.rnc | 3 ++- src/xrc/xh_datectrl.cpp | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/misc/schema/xrc_schema.rnc b/misc/schema/xrc_schema.rnc index 8b63cbe642..23a132f74b 100644 --- a/misc/schema/xrc_schema.rnc +++ b/misc/schema/xrc_schema.rnc @@ -894,7 +894,8 @@ wxDatePickerCtrl = element object { attribute class { "wxDatePickerCtrl" } & stdObjectNodeAttributes & - stdWindowProperties + stdWindowProperties & + [xrc:p="o"] element null-text {_, t_text }* } diff --git a/src/xrc/xh_datectrl.cpp b/src/xrc/xh_datectrl.cpp index 5255e75864..6ee4f99f51 100644 --- a/src/xrc/xh_datectrl.cpp +++ b/src/xrc/xh_datectrl.cpp @@ -42,6 +42,10 @@ wxObject *wxDateCtrlXmlHandler::DoCreateResource() SetupWindow(picker); + // Note that we want to set this one even if it's empty. + if ( HasParam(wxS("null-text")) ) + picker->SetNullText(GetText(wxS("null-text"))); + return picker; }