2 * Copyright (C) 2005-2018 Team Kodi
3 * This file is part of Kodi - https://kodi.tv
5 * SPDX-License-Identifier: GPL-2.0-or-later
6 * See LICENSES/README.md for more information.
9 #include "GUIDialogNumeric.h"
11 #include "ServiceBroker.h"
12 #include "XBDateTime.h"
13 #include "guilib/GUIComponent.h"
14 #include "guilib/GUILabelControl.h"
15 #include "guilib/GUIWindowManager.h"
16 #include "guilib/LocalizeStrings.h"
17 #include "input/actions/Action.h"
18 #include "input/actions/ActionIDs.h"
19 #include "input/keyboard/KeyIDs.h"
20 #include "input/keyboard/XBMC_vkeys.h"
21 #include "interfaces/AnnouncementManager.h"
22 #include "messaging/helpers/DialogOKHelper.h"
23 #include "utils/Digest.h"
24 #include "utils/StringUtils.h"
25 #include "utils/Variant.h"
29 #define CONTROL_HEADING_LABEL 1
30 #define CONTROL_INPUT_LABEL 4
31 #define CONTROL_NUM0 10
32 #define CONTROL_NUM9 19
33 #define CONTROL_PREVIOUS 20
34 #define CONTROL_ENTER 21
35 #define CONTROL_NEXT 22
36 #define CONTROL_BACKSPACE 23
38 using namespace KODI::MESSAGING
;
39 using KODI::UTILITY::CDigest
;
41 CGUIDialogNumeric::CGUIDialogNumeric(void) : CGUIDialog(WINDOW_DIALOG_NUMERIC
, "DialogNumeric.xml")
43 memset(&m_datetime
, 0, sizeof(KODI::TIME::SystemTime
));
44 m_loadType
= KEEP_IN_MEMORY
;
47 CGUIDialogNumeric::~CGUIDialogNumeric(void) = default;
49 void CGUIDialogNumeric::OnInitWindow()
51 CGUIDialog::OnInitWindow();
57 data
["type"] = "time";
60 data
["type"] = "date";
62 case INPUT_IP_ADDRESS
:
66 data
["type"] = "numericpassword";
69 data
["type"] = "number";
71 case INPUT_TIME_SECONDS
:
72 data
["type"] = "seconds";
75 data
["type"] = "keyboard";
79 const CGUIControl
*control
= GetControl(CONTROL_HEADING_LABEL
);
80 if (control
!= nullptr)
81 data
["title"] = control
->GetDescription();
83 data
["value"] = GetOutputString();
85 CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::Input
, "OnInputRequested", data
);
88 void CGUIDialogNumeric::OnDeinitWindow(int nextWindowID
)
91 CGUIDialog::OnDeinitWindow(nextWindowID
);
93 CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::Input
, "OnInputFinished");
96 bool CGUIDialogNumeric::OnAction(const CAction
&action
)
98 if (action
.GetID() == ACTION_NEXT_ITEM
)
100 else if (action
.GetID() == ACTION_PREV_ITEM
)
102 else if (action
.GetID() == ACTION_BACKSPACE
)
104 else if (action
.GetID() == ACTION_ENTER
)
106 else if (action
.GetID() >= REMOTE_0
&& action
.GetID() <= REMOTE_9
)
107 OnNumber(action
.GetID() - REMOTE_0
);
108 else if (action
.GetID() >= KEY_VKEY
&& action
.GetID() < KEY_UNICODE
)
110 // input from the keyboard (vkey, not ascii)
111 uint8_t b
= action
.GetID() & 0xFF;
112 if (b
== XBMCVK_LEFT
)
114 else if (b
== XBMCVK_RIGHT
)
116 else if (b
== XBMCVK_RETURN
|| b
== XBMCVK_NUMPADENTER
)
118 else if (b
== XBMCVK_BACK
)
120 else if (b
== XBMCVK_ESCAPE
)
123 else if (action
.GetID() == KEY_UNICODE
)
124 { // input from the keyboard
125 if (action
.GetUnicode() == 10 || action
.GetUnicode() == 13)
127 else if (action
.GetUnicode() == 8)
128 OnBackSpace(); // backspace
129 else if (action
.GetUnicode() == 27)
130 OnCancel(); // escape
131 else if (action
.GetUnicode() == 46)
133 else if (action
.GetUnicode() >= 48 && action
.GetUnicode() < 58) // number
134 OnNumber(action
.GetUnicode() - 48);
137 return CGUIDialog::OnAction(action
);
142 bool CGUIDialogNumeric::OnBack(int actionID
)
148 bool CGUIDialogNumeric::OnMessage(CGUIMessage
& message
)
150 switch ( message
.GetMessage() )
152 case GUI_MSG_WINDOW_INIT
:
154 m_bConfirmed
= false;
157 return CGUIDialog::OnMessage(message
);
161 case GUI_MSG_CLICKED
:
163 int iControl
= message
.GetSenderId();
164 m_bConfirmed
= false;
166 if (CONTROL_NUM0
<= iControl
&& iControl
<= CONTROL_NUM9
) // User numeric entry via dialog button UI
168 OnNumber(iControl
- 10);
171 else if (iControl
== CONTROL_PREVIOUS
)
176 else if (iControl
== CONTROL_NEXT
)
181 else if (iControl
== CONTROL_BACKSPACE
)
186 else if (iControl
== CONTROL_ENTER
)
194 case GUI_MSG_SET_TEXT
:
195 SetMode(m_mode
, message
.GetLabel());
197 // close the dialog if requested
198 if (message
.GetParam1() > 0)
202 return CGUIDialog::OnMessage(message
);
205 void CGUIDialogNumeric::OnBackSpace()
207 if (!m_dirty
&& m_block
)
212 if (m_mode
== INPUT_NUMBER
|| m_mode
== INPUT_PASSWORD
)
213 { // just go back one character
214 if (!m_number
.empty())
215 m_number
.erase(m_number
.length() - 1);
217 else if (m_mode
== INPUT_IP_ADDRESS
)
227 else if (m_mode
== INPUT_TIME
)
230 m_datetime
.hour
/= 10;
231 else if (m_datetime
.minute
)
232 m_datetime
.minute
/= 10;
239 else if (m_mode
== INPUT_TIME_SECONDS
)
242 m_datetime
.hour
/= 10;
243 else if (m_block
== 1)
245 if (m_datetime
.minute
)
246 m_datetime
.minute
/= 10;
253 else if (m_datetime
.second
)
254 m_datetime
.minute
/= 10;
261 else if (m_mode
== INPUT_DATE
)
264 m_datetime
.day
/= 10;
265 else if (m_block
== 1)
267 if (m_datetime
.month
)
268 m_datetime
.month
/= 10;
275 else if (m_datetime
.year
) // m_block == 2
276 m_datetime
.year
/= 10;
285 void CGUIDialogNumeric::OnPrevious()
292 void CGUIDialogNumeric::OnNext()
294 if (m_mode
== INPUT_IP_ADDRESS
&& m_block
==0 && m_ip
[0]==0)
297 if (m_block
< m_lastblock
)
300 if (m_mode
== INPUT_DATE
)
301 VerifyDate(m_block
== 2);
304 void CGUIDialogNumeric::FrameMove()
306 std::string strLabel
;
307 unsigned int start
= 0;
308 unsigned int end
= 0;
309 if (m_mode
== INPUT_PASSWORD
)
310 strLabel
.assign(m_number
.length(), '*');
311 else if (m_mode
== INPUT_NUMBER
)
313 else if (m_mode
== INPUT_TIME
)
314 { // format up the time
315 strLabel
= StringUtils::Format("{:2}:{:02}", m_datetime
.hour
, m_datetime
.minute
);
317 end
= m_block
* 3 + 2;
319 else if (m_mode
== INPUT_TIME_SECONDS
)
320 { // format up the time
321 strLabel
= StringUtils::Format("{:2}:{:02}:{:02}", m_datetime
.hour
, m_datetime
.minute
,
324 end
= m_block
* 3 + 2;
326 else if (m_mode
== INPUT_DATE
)
327 { // format up the date
329 StringUtils::Format("{:2}/{:2}/{:4}", m_datetime
.day
, m_datetime
.month
, m_datetime
.year
);
331 end
= m_block
* 3 + 2;
333 end
= m_block
* 3 + 4;
335 else if (m_mode
== INPUT_IP_ADDRESS
)
336 { // format up the date
337 strLabel
= StringUtils::Format("{:3}.{:3}.{:3}.{:3}", m_ip
[0], m_ip
[1], m_ip
[2], m_ip
[3]);
339 end
= m_block
* 4 + 3;
341 CGUILabelControl
*pLabel
= dynamic_cast<CGUILabelControl
*>(GetControl(CONTROL_INPUT_LABEL
));
344 pLabel
->SetLabel(strLabel
);
345 pLabel
->SetHighlight(start
, end
);
347 CGUIDialog::FrameMove();
350 void CGUIDialogNumeric::OnNumber(uint32_t num
)
358 m_number
+= num
+ '0';
361 HandleInputTime(num
);
363 case INPUT_TIME_SECONDS
:
364 HandleInputSeconds(num
);
367 HandleInputDate(num
);
369 case INPUT_IP_ADDRESS
:
375 void CGUIDialogNumeric::SetMode(INPUT_MODE mode
, const KODI::TIME::SystemTime
& initial
)
380 if (m_mode
== INPUT_TIME
|| m_mode
== INPUT_TIME_SECONDS
|| m_mode
== INPUT_DATE
)
382 m_datetime
= initial
;
383 m_lastblock
= (m_mode
!= INPUT_TIME
) ? 2 : 1;
387 void CGUIDialogNumeric::SetMode(INPUT_MODE mode
, const std::string
&initial
)
392 if (m_mode
== INPUT_TIME
|| m_mode
== INPUT_TIME_SECONDS
|| m_mode
== INPUT_DATE
)
395 if (m_mode
== INPUT_TIME
|| m_mode
== INPUT_TIME_SECONDS
)
397 // check if we have a pure number
398 if (initial
.find_first_not_of("0123456789") == std::string::npos
)
400 long seconds
= strtol(initial
.c_str(), nullptr, 10);
405 std::string tmp
= initial
;
406 // if we are handling seconds and if the string only contains
407 // "mm:ss" we need to add dummy "hh:" to get "hh:mm:ss"
408 if (m_mode
== INPUT_TIME_SECONDS
&& tmp
.length() <= 5)
410 dateTime
.SetFromDBTime(tmp
);
413 else if (m_mode
== INPUT_DATE
)
415 std::string tmp
= initial
;
416 StringUtils::Replace(tmp
, '/', '.');
417 dateTime
.SetFromDBDate(tmp
);
420 if (!dateTime
.IsValid())
423 dateTime
.GetAsSystemTime(m_datetime
);
424 m_lastblock
= (m_mode
== INPUT_DATE
) ? 2 : 1;
426 else if (m_mode
== INPUT_IP_ADDRESS
)
429 auto blocks
= StringUtils::Split(initial
, '.');
430 if (blocks
.size() != 4)
433 for (size_t i
= 0; i
< blocks
.size(); ++i
)
435 if (blocks
[i
].length() > 3)
438 m_ip
[i
] = static_cast<uint8_t>(atoi(blocks
[i
].c_str()));
441 else if (m_mode
== INPUT_NUMBER
|| m_mode
== INPUT_PASSWORD
)
445 KODI::TIME::SystemTime
CGUIDialogNumeric::GetOutput() const
447 assert(m_mode
== INPUT_TIME
|| m_mode
== INPUT_TIME_SECONDS
|| m_mode
== INPUT_DATE
);
451 std::string
CGUIDialogNumeric::GetOutputString() const
456 return StringUtils::Format("{:02}/{:02}/{:04}", m_datetime
.day
, m_datetime
.month
,
459 return StringUtils::Format("{}:{:02}", m_datetime
.hour
, m_datetime
.minute
);
460 case INPUT_TIME_SECONDS
:
461 return StringUtils::Format("{}:{:02}:{:02}", m_datetime
.hour
, m_datetime
.minute
,
463 case INPUT_IP_ADDRESS
:
464 return StringUtils::Format("{}.{}.{}.{}", m_ip
[0], m_ip
[1], m_ip
[2], m_ip
[3]);
470 //should never get here
471 return std::string();
474 bool CGUIDialogNumeric::ShowAndGetSeconds(std::string
&timeString
, const std::string
&heading
)
476 CGUIDialogNumeric
*pDialog
= CServiceBroker::GetGUI()->GetWindowManager().GetWindow
<CGUIDialogNumeric
>(WINDOW_DIALOG_NUMERIC
);
477 if (!pDialog
) return false;
478 int seconds
= StringUtils::TimeStringToSeconds(timeString
);
479 KODI::TIME::SystemTime time
= {};
480 time
.hour
= seconds
/ 3600;
481 time
.minute
= (seconds
- time
.hour
* 3600) / 60;
482 time
.second
= seconds
- time
.hour
* 3600 - time
.minute
* 60;
483 pDialog
->SetMode(INPUT_TIME_SECONDS
, time
);
484 pDialog
->SetHeading(heading
);
486 if (!pDialog
->IsConfirmed() || pDialog
->IsCanceled())
488 time
= pDialog
->GetOutput();
489 seconds
= time
.hour
* 3600 + time
.minute
* 60 + time
.second
;
490 timeString
= StringUtils::SecondsToTimeString(seconds
);
494 bool CGUIDialogNumeric::ShowAndGetTime(KODI::TIME::SystemTime
& time
, const std::string
& heading
)
496 CGUIDialogNumeric
*pDialog
= CServiceBroker::GetGUI()->GetWindowManager().GetWindow
<CGUIDialogNumeric
>(WINDOW_DIALOG_NUMERIC
);
497 if (!pDialog
) return false;
498 pDialog
->SetMode(INPUT_TIME
, time
);
499 pDialog
->SetHeading(heading
);
501 if (!pDialog
->IsConfirmed() || pDialog
->IsCanceled())
503 time
= pDialog
->GetOutput();
507 bool CGUIDialogNumeric::ShowAndGetDate(KODI::TIME::SystemTime
& date
, const std::string
& heading
)
509 CGUIDialogNumeric
*pDialog
= CServiceBroker::GetGUI()->GetWindowManager().GetWindow
<CGUIDialogNumeric
>(WINDOW_DIALOG_NUMERIC
);
510 if (!pDialog
) return false;
511 pDialog
->SetMode(INPUT_DATE
, date
);
512 pDialog
->SetHeading(heading
);
514 if (!pDialog
->IsConfirmed() || pDialog
->IsCanceled())
516 date
= pDialog
->GetOutput();
520 bool CGUIDialogNumeric::ShowAndGetIPAddress(std::string
&IPAddress
, const std::string
&heading
)
522 CGUIDialogNumeric
*pDialog
= CServiceBroker::GetGUI()->GetWindowManager().GetWindow
<CGUIDialogNumeric
>(WINDOW_DIALOG_NUMERIC
);
523 if (!pDialog
) return false;
524 pDialog
->SetMode(INPUT_IP_ADDRESS
, IPAddress
);
525 pDialog
->SetHeading(heading
);
527 if (!pDialog
->IsConfirmed() || pDialog
->IsCanceled())
529 IPAddress
= pDialog
->GetOutputString();
533 bool CGUIDialogNumeric::ShowAndGetNumber(std::string
& strInput
, const std::string
&strHeading
, unsigned int iAutoCloseTimeoutMs
/* = 0 */, bool bSetHidden
/* = false */)
535 // Prompt user for password input
536 CGUIDialogNumeric
*pDialog
= CServiceBroker::GetGUI()->GetWindowManager().GetWindow
<CGUIDialogNumeric
>(WINDOW_DIALOG_NUMERIC
);
537 pDialog
->SetHeading( strHeading
);
540 pDialog
->SetMode(INPUT_PASSWORD
, strInput
);
542 pDialog
->SetMode(INPUT_NUMBER
, strInput
);
543 if (iAutoCloseTimeoutMs
)
544 pDialog
->SetAutoClose(iAutoCloseTimeoutMs
);
548 if (!pDialog
->IsAutoClosed() && (!pDialog
->IsConfirmed() || pDialog
->IsCanceled()))
550 strInput
= pDialog
->GetOutputString();
554 // \brief Show numeric keypad twice to get and confirm a user-entered password string.
555 // \param strNewPassword String to preload into the keyboard accumulator. Overwritten with user input if return=true.
556 // \return true if successful display and user input entry/re-entry. false if unsuccessful display, no user input, or canceled editing.
557 bool CGUIDialogNumeric::ShowAndVerifyNewPassword(std::string
& strNewPassword
)
559 // Prompt user for password input
560 std::string strUserInput
;
561 InputVerificationResult ret
= ShowAndVerifyInput(strUserInput
, g_localizeStrings
.Get(12340), false);
562 if (ret
!= InputVerificationResult::SUCCESS
)
564 if (ret
== InputVerificationResult::FAILED
)
566 // Show error to user saying the password entry was blank
567 HELPERS::ShowOKDialogText(CVariant
{12357}, CVariant
{12358}); // Password is empty/blank
572 if (strUserInput
.empty())
576 // Prompt again for password input, this time sending previous input as the password to verify
577 ret
= ShowAndVerifyInput(strUserInput
, g_localizeStrings
.Get(12341), true);
578 if (ret
!= InputVerificationResult::SUCCESS
)
580 if (ret
== InputVerificationResult::FAILED
)
582 // Show error to user saying the password re-entry failed
583 HELPERS::ShowOKDialogText(CVariant
{12357}, CVariant
{12344}); // Password do not match
588 // password entry and re-entry succeeded
589 strNewPassword
= strUserInput
;
593 // \brief Show numeric keypad and verify user input against strPassword.
594 // \param strPassword Value to compare against user input.
595 // \param strHeading String shown on dialog title. Converts to localized string if contains a positive integer.
596 // \param iRetries If greater than 0, shows "Incorrect password, %d retries left" on dialog line 2, else line 2 is blank.
597 // \return 0 if successful display and user input. 1 if unsuccessful input. -1 if no user input or canceled editing.
598 int CGUIDialogNumeric::ShowAndVerifyPassword(std::string
& strPassword
, const std::string
& strHeading
, int iRetries
)
600 std::string strTempHeading
= strHeading
;
603 // Show a string telling user they have iRetries retries left
604 strTempHeading
= StringUtils::Format("{}. {} {} {}", strHeading
, g_localizeStrings
.Get(12342),
605 iRetries
, g_localizeStrings
.Get(12343));
608 // make a copy of strPassword to prevent from overwriting it later
609 std::string strPassTemp
= strPassword
;
610 InputVerificationResult ret
= ShowAndVerifyInput(strPassTemp
, strTempHeading
, true);
611 if (ret
== InputVerificationResult::SUCCESS
)
612 return 0; // user entered correct password
614 if (ret
== InputVerificationResult::CANCELED
)
615 return -1; // user canceled out
617 return 1; // user must have entered an incorrect password
620 // \brief Show numeric keypad and verify user input against strToVerify.
621 // \param strToVerify Value to compare against user input.
622 // \param dlgHeading String shown on dialog title.
623 // \param bVerifyInput If set as true we verify the users input versus strToVerify.
624 // \return the result of the check (success, failed, or canceled by user).
625 InputVerificationResult
CGUIDialogNumeric::ShowAndVerifyInput(std::string
& strToVerify
, const std::string
& dlgHeading
, bool bVerifyInput
)
627 // Prompt user for password input
628 CGUIDialogNumeric
*pDialog
= CServiceBroker::GetGUI()->GetWindowManager().GetWindow
<CGUIDialogNumeric
>(WINDOW_DIALOG_NUMERIC
);
629 pDialog
->SetHeading(dlgHeading
);
631 std::string strInput
;
633 strInput
= strToVerify
;
635 pDialog
->SetMode(INPUT_PASSWORD
, strInput
);
638 strInput
= pDialog
->GetOutputString();
640 if (!pDialog
->IsConfirmed() || pDialog
->IsCanceled())
644 return InputVerificationResult::CANCELED
;
647 const std::string md5pword2
= CDigest::Calculate(CDigest::Type::MD5
, strInput
);
651 strToVerify
= md5pword2
;
652 return InputVerificationResult::SUCCESS
;
655 return StringUtils::EqualsNoCase(strToVerify
, md5pword2
) ? InputVerificationResult::SUCCESS
: InputVerificationResult::FAILED
;
658 bool CGUIDialogNumeric::IsConfirmed() const
663 bool CGUIDialogNumeric::IsCanceled() const
668 void CGUIDialogNumeric::SetHeading(const std::string
& strHeading
)
671 CGUIMessage
msg(GUI_MSG_LABEL_SET
, GetID(), CONTROL_HEADING_LABEL
);
672 msg
.SetLabel(strHeading
);
676 void CGUIDialogNumeric::VerifyDate(bool checkYear
)
678 if (m_datetime
.day
== 0)
680 if (m_datetime
.month
== 0)
681 m_datetime
.month
= 1;
682 // check for number of days in the month
683 if (m_datetime
.day
== 31)
685 if (m_datetime
.month
== 4 || m_datetime
.month
== 6 || m_datetime
.month
== 9 ||
686 m_datetime
.month
== 11)
689 if (m_datetime
.month
== 2 && m_datetime
.day
> 28)
691 m_datetime
.day
= 29; // max in february.
694 // leap years occur when the year is divisible by 4 but not by 100, or the year is divisible by 400
695 // thus they don't occur, if the year has a remainder when divided by 4, or when the year is divisible by 100 but not by 400
696 if ((m_datetime
.year
% 4) || (!(m_datetime
.year
% 100) && (m_datetime
.year
% 400)))
702 void CGUIDialogNumeric::OnOK()
709 void CGUIDialogNumeric::OnCancel()
711 m_bConfirmed
= false;
716 void CGUIDialogNumeric::HandleInputIP(uint32_t num
)
718 if (m_dirty
&& ((m_ip
[m_block
] < 25) || (m_ip
[m_block
] == 25 && num
< 6) || !(m_block
== 0 && num
== 0)))
721 m_ip
[m_block
] += num
;
726 if (m_ip
[m_block
] > 25 || (m_ip
[m_block
] == 0 && num
== 0))
737 void CGUIDialogNumeric::HandleInputDate(uint32_t num
)
739 if (m_block
== 0) // day of month
741 if (m_dirty
&& (m_datetime
.day
< 3 || num
< 2))
743 m_datetime
.day
*= 10;
744 m_datetime
.day
+= num
;
747 m_datetime
.day
= num
;
749 if (m_datetime
.day
> 3)
751 m_block
= 1; // move to months
757 else if (m_block
== 1) // months
759 if (m_dirty
&& num
< 3)
761 m_datetime
.month
*= 10;
762 m_datetime
.month
+= num
;
765 m_datetime
.month
= num
;
767 if (m_datetime
.month
> 1)
770 m_block
= 2; // move to year
778 if (m_dirty
&& m_datetime
.year
< 1000) // have taken input
780 m_datetime
.year
*= 10;
781 m_datetime
.year
+= num
;
784 m_datetime
.year
= num
;
786 if (m_datetime
.year
> 1000)
789 m_block
= 0; // move to day of month
797 void CGUIDialogNumeric::HandleInputSeconds(uint32_t num
)
799 if (m_block
== 0) // hour
801 if (m_dirty
) // have input the first digit
803 m_datetime
.hour
*= 10;
804 m_datetime
.hour
+= num
;
805 m_block
= 1; // move to minutes - allows up to 99 hours
808 else // this is the first digit
810 m_datetime
.hour
= num
;
814 else if (m_block
== 1) // minute
816 if (m_dirty
) // have input the first digit
818 m_datetime
.minute
*= 10;
819 m_datetime
.minute
+= num
;
820 m_block
= 2; // move to seconds - allows up to 99 minutes
823 else // this is the first digit
825 m_datetime
.minute
= num
;
828 m_block
= 2; // move to seconds
837 if (m_dirty
) // have input the first digit
839 m_datetime
.second
*= 10;
840 m_datetime
.second
+= num
;
841 m_block
= 0; // move to hours
844 else // this is the first digit
846 m_datetime
.second
= num
;
849 m_block
= 0; // move to hours
858 void CGUIDialogNumeric::HandleInputTime(uint32_t num
)
860 if (m_block
== 0) // hour
862 if (m_dirty
) // have input the first digit
864 if (m_datetime
.hour
< 2 || num
< 4)
866 m_datetime
.hour
*= 10;
867 m_datetime
.hour
+= num
;
870 m_datetime
.hour
= num
;
872 m_block
= 1; // move to minutes
875 else // this is the first digit
877 m_datetime
.hour
= num
;
881 m_block
= 1; // move to minutes
890 if (m_dirty
) // have input the first digit
892 m_datetime
.minute
*= 10;
893 m_datetime
.minute
+= num
;
894 m_block
= 0; // move to hours
897 else // this is the first digit
899 m_datetime
.minute
= num
;
903 m_block
= 0; // move to hours