From fad9931a6040d5e12d90220596e11187fc671f67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zden=C4=9Bk=20Frydr=C3=BDn?= <51875432+adidas-official@users.noreply.github.com> Date: Mon, 16 Dec 2024 16:05:57 +0100 Subject: [PATCH] Feature dateentry (#7) datetime widget gui * Tags included, preserve time Tags now control what is available to display in a widget. We can change date/time, date only and time only. Full precision tag controls if we see miliseconds. Time is also preserved/updated when we change the date in calendar. Calendar is only available if the `tag.date` is true. * Removed width of entry fields * Implementing method increment_part This method is a result of refactoring change_date. Logic that checks if the entry has a valid values is simplified and the amount of code is reduced. If value is invalid, the entry won't change and no exception is raised * Fixed creating multiple entries All widgets had another 2 invisible spinboxes. With time they were visible right away, in case of date and datetime they were revealed with opening a calendar * Time changes hours by default In spinbox with only time, default behaviour of the arrows is to increase/decrease hours instead of minutes --------- Co-authored-by: zf Co-authored-by: zfydryn --- mininterface/tk_interface/date_entry.py | 174 +++++++++++++++++------- mininterface/tk_interface/utils.py | 1 + mininterface/types.py | 4 +- 3 files changed, 126 insertions(+), 53 deletions(-) diff --git a/mininterface/tk_interface/date_entry.py b/mininterface/tk_interface/date_entry.py index 169c2d2..0dacd07 100644 --- a/mininterface/tk_interface/date_entry.py +++ b/mininterface/tk_interface/date_entry.py @@ -14,11 +14,26 @@ class DateEntryFrame(tk.Frame): + last_date_entry_frame = None + def __init__(self, master, tk_app: "TkWindow", tag: DatetimeTag, variable: tk.Variable, **kwargs): super().__init__(master, **kwargs) self.tk_app = tk_app self.tag = tag + if tag.date and tag.time: + if tag.full_precision: + self.datetimeformat = '%Y-%m-%d %H:%M:%S.%f' + else: + self.datetimeformat = '%Y-%m-%d %H:%M:%S' + elif tag.time and not tag.date: + if tag.full_precision: + self.datetimeformat = '%H:%M:%S.%f' + else: + self.datetimeformat = '%H:%M:%S' + else: + self.datetimeformat = '%Y-%m-%d' + # Date entry self.spinbox = self.create_spinbox(variable) @@ -27,7 +42,7 @@ def __init__(self, master, tk_app: "TkWindow", tag: DatetimeTag, variable: tk.Va self.frame = tk.Frame(self) # The calendar widget - if Calendar: + if Calendar and tag.date: # Toggle calendar button tk.Button(self, text="…", command=self.toggle_calendar).grid(row=0, column=1) @@ -37,19 +52,23 @@ def __init__(self, master, tk_app: "TkWindow", tag: DatetimeTag, variable: tk.Va self.calendar.bind("<>", self.on_date_select) self.calendar.grid() # Initialize calendar with the current date - self.update_calendar(self.spinbox.get(), '%Y-%m-%d %H:%M:%S.%f') + self.update_calendar(self.spinbox.get(), self.datetimeformat) + DateEntryFrame.last_date_entry_frame = self else: self.calendar = None self.bind_all_events() def create_spinbox(self, variable: tk.Variable): - spinbox = tk.Spinbox(self, font=("Arial", 16), width=30, wrap=True, textvariable=variable) + spinbox = tk.Spinbox(self, wrap=True, textvariable=variable) spinbox.grid() if not variable.get(): - spinbox.insert(0, datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-4]) + spinbox.insert(0, datetime.now().strftime(self.datetimeformat)) spinbox.focus_set() - spinbox.icursor(8) + if (not self.tag.date and self.tag.time): + spinbox.icursor(0) + else: + spinbox.icursor(8) # Bind up/down arrow keys spinbox.bind("", self.increment_value) @@ -60,6 +79,10 @@ def create_spinbox(self, variable: tk.Variable): # Bind key release event to update calendar when user changes the input field spinbox.bind("", self.on_spinbox_change) + + # Toggle calendar widget with ctrl+shift+c + spinbox.bind("", self.toggle_calendar) + return spinbox def bind_all_events(self): @@ -72,9 +95,6 @@ def bind_all_events(self): # Paste from clipboard with ctrl+v self.bind_all("", lambda event: self.paste_from_clipboard()) - # Toggle calendar widget with ctrl+shift+c - self.bind_all("", lambda event: self.toggle_calendar()) - def toggle_calendar(self, event=None): if not self.calendar: return @@ -102,7 +122,10 @@ def find_valid_date(self): def find_valid_time(self): input = self.spinbox.get() # use regex to find the time part - time_part = re.search(r'\d{2}:\d{2}:\d{2}', input) + if self.tag.full_precision: + time_part = re.search(r'\d{2}:\d{2}:\d{2}.\d{6}', input) + else: + time_part = re.search(r'\d{2}:\d{2}:\d{2}', input) if time_part: return time_part.group() return False @@ -114,43 +137,62 @@ def change_date(self, delta): date = self.find_valid_date() time = self.find_valid_time() - if date: + if date and not time: + split_input = re.split(r'[-]', date) + new_value_str = self.increment_part(split_input, caret_pos, delta, '-') + elif date and time: split_input = re.split(r'[- :.]', date_str) - part_index = self.get_part_index(caret_pos, len(split_input)) + new_value_str = self.increment_part(split_input, caret_pos, delta, ' ') + elif not date and time: + split_input = re.split(r'[:.]', time) + new_value_str = self.increment_part(split_input, caret_pos, delta, ':') + else: + return - # Increment or decrement the relevant part - number = int(split_input[part_index]) - new_number = number + delta - split_input[part_index] = str(new_number).zfill(len(split_input[part_index])) + # Validate the new date + try: + datetime.strptime(new_value_str, self.datetimeformat) + self.spinbox.delete(0, tk.END) + self.spinbox.insert(0, new_value_str) + self.spinbox.icursor(caret_pos) + if Calendar: + self.update_calendar(new_value_str, self.datetimeformat) + except ValueError as e: + pass - if time: - new_value_str = f"{split_input[0]}-{split_input[1]}-{split_input[2]} "\ - f"{split_input[3]}:{split_input[4]}:{split_input[5]}.{split_input[6][:2]}" - string_format = '%Y-%m-%d %H:%M:%S.%f' + def increment_part(self, split_input, caret_pos, delta, separator): + part_index = self.get_part_index(caret_pos) + if part_index > len(split_input) - 1: + return separator.join(split_input) + + # Increment or decrement the relevant part + number = int(split_input[part_index]) + new_number = number + delta + split_input[part_index] = str(new_number).zfill(len(split_input[part_index])) + + if self.tag.full_precision and separator == ' ': + return f"{split_input[0]}-{split_input[1]}-{split_input[2]} "\ + f"{split_input[3]}:{split_input[4]}:{split_input[5]}.{split_input[6]}" + elif separator == ' ': + return f"{split_input[0]}-{split_input[1]}-{split_input[2]} "\ + f"{split_input[3]}:{split_input[4]}:{split_input[5]}" + elif separator == ':': + if self.tag.full_precision: + return f"{split_input[0]}:{split_input[1]}:{split_input[2]}.{split_input[3]}" else: - new_value_str = f"{split_input[0]}-{split_input[1]}-{split_input[2]}" - string_format = '%Y-%m-%d' - - # Validate the new date - try: - datetime.strptime(new_value_str, string_format) - self.spinbox.delete(0, tk.END) - self.spinbox.insert(0, new_value_str) - self.spinbox.icursor(caret_pos) - if Calendar: - self.update_calendar(new_value_str, string_format) - except ValueError: - pass - - def get_part_index(self, caret_pos, split_length): - if caret_pos < 5: # year - return 0 - elif caret_pos < 8: # month - return 1 - elif caret_pos < 11: # day - return 2 - elif split_length > 3: - if caret_pos < 14: # hour + return f"{split_input[0]}:{split_input[1]}:{split_input[2]}" + else: + return separator.join(split_input) + + def get_part_index(self, caret_pos): + if self.tag.date and self.tag.time: + if caret_pos < 5: # year + return 0 + elif caret_pos < 8: # month + return 1 + elif caret_pos < 11: # day + return 2 + elif caret_pos < 14: # hour return 3 elif caret_pos < 17: # minute return 4 @@ -158,7 +200,23 @@ def get_part_index(self, caret_pos, split_length): return 5 else: # millisecond return 6 - return 2 + elif self.tag.date: + if caret_pos < 5: # year + return 0 + elif caret_pos < 8: # month + return 1 + elif caret_pos < 11: # day + return 2 + elif self.tag.time: + if caret_pos < 3: # hour + return 0 + elif caret_pos < 6: # minute + return 1 + elif caret_pos < 9: # second + return 2 + else: # millisecond + return 3 + return 0 def on_spinbox_click(self, event): # Check if the click was on the spinbox arrows @@ -168,9 +226,17 @@ def on_spinbox_click(self, event): self.decrement_value() def on_date_select(self, event): - selected_date = self.calendar.selection_get() + + selected_date = self.calendar.selection_get().strftime('%Y-%m-%d') + if self.tag.time: + if self.tag.full_precision: + current_time = datetime.now().strftime('%H:%M:%S.%f') + else: + current_time = datetime.now().strftime('%H:%M:%S') + selected_date += f" {current_time}" + self.spinbox.delete(0, tk.END) - self.spinbox.insert(0, selected_date.strftime('%Y-%m-%d')) + self.spinbox.insert(0, selected_date) self.spinbox.icursor(len(self.spinbox.get())) def on_spinbox_change(self, event): @@ -178,11 +244,12 @@ def on_spinbox_change(self, event): self.update_calendar(self.spinbox.get()) def update_calendar(self, date_str, string_format='%Y-%m-%d'): - try: - date = datetime.strptime(date_str, string_format) - self.calendar.selection_set(date) - except ValueError: - pass + if self.tag.date: + try: + date = datetime.strptime(date_str, string_format) + self.calendar.selection_set(date) + except ValueError: + pass def copy_to_clipboard(self, event=None): self.clipboard_clear() @@ -194,7 +261,7 @@ def show_popup(self, message): popup = tk.Toplevel(self) popup.wm_title("") - label = tk.Label(popup, text=message, font=("Arial", 12)) + label = tk.Label(popup, text=message) label.pack(side="top", fill="x", pady=10, padx=10) # Position the popup window in the top-left corner of the widget @@ -219,3 +286,8 @@ def select_all(self, event=None): def paste_from_clipboard(self, event=None): self.spinbox.delete(0, tk.END) self.spinbox.insert(0, self.clipboard_get()) + + def round_time(self, dt): + if self.tag.full_precision: + return dt + return dt[:-4] diff --git a/mininterface/tk_interface/utils.py b/mininterface/tk_interface/utils.py index e9db548..0bf858e 100644 --- a/mininterface/tk_interface/utils.py +++ b/mininterface/tk_interface/utils.py @@ -136,6 +136,7 @@ def _fetch(variable): # Calendar elif isinstance(tag, DatetimeTag): grid_info = widget.grid_info() + widget.grid_forget() # HERE nested_frame = DateEntryFrame(master, tk_app, tag, variable) nested_frame.grid(row=grid_info['row'], column=grid_info['column']) widget = nested_frame.spinbox diff --git a/mininterface/types.py b/mininterface/types.py index 9f88da9..f9d6833 100644 --- a/mininterface/types.py +++ b/mininterface/types.py @@ -230,8 +230,8 @@ def __post_init__(self): if self.annotation: self.date = issubclass(self.annotation, date) self.time = issubclass(self.annotation, time) or issubclass(self.annotation, datetime) - if not self.date and not self.time: - self.date = self.time = True + if not self.time and self.full_precision: + self.full_precision = False # NOTE: self.full_precision ... def _make_default_value(self):