Date picker to support IfcDateTime

See https://imgur.com/a/9a7JfMO
This commit is contained in:
Andrej730
2025-01-17 14:35:47 +05:00
parent fa98a7e8f7
commit e8cb9e7f3c
5 changed files with 112 additions and 34 deletions
+4 -1
View File
@@ -98,9 +98,10 @@ def draw_attribute(
if attribute.is_uri:
op = layout.operator("bim.select_uri_attribute", text="", icon="FILE_FOLDER")
op.data_path = attribute.path_from_id("string_value")
elif attribute.special_type == "DATE":
elif attribute.special_type in ("DATE", "DATETIME"):
op = layout.operator("bim.datepicker", text="", icon="TIME")
op.target_prop = attribute.path_from_id("string_value")
op.include_time = attribute.special_type == "DATETIME"
if attribute.is_optional:
layout.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="")
@@ -170,6 +171,8 @@ def import_attribute(
new.is_uri = True
elif attribute.type_of_attribute()._is("IfcDate"):
new.special_type = "DATE"
elif attribute.type_of_attribute()._is("IfcDateTime"):
new.special_type = "DATETIME"
elif data_type == "boolean":
new.bool_value = False if new.is_null else bool(data[attribute.name()])
elif data_type == "integer":
@@ -38,6 +38,7 @@ def parse_duration(value):
def canonicalise_time(time: Union[datetime, None]) -> str:
"""Actualy canonicalises datetime as just a date, time is not included."""
if not time:
return "-"
return time.strftime("%d/%m/%y")
@@ -1178,27 +1178,45 @@ class Bonsai_DatePicker(bpy.types.Operator):
bl_label = "Date Picker"
bl_idname = "bim.datepicker"
bl_options = {"REGISTER", "UNDO"}
display_date: bpy.props.StringProperty(name="Display Date")
target_prop: bpy.props.StringProperty(name="Target date prop to set")
# TODO: base it on property type.
include_time: bpy.props.BoolProperty(name="Include Time", default=True)
if TYPE_CHECKING:
target_prop: str
include_time: bool
def execute(self, context):
selected_date = context.scene.DatePickerProperties.selected_date
try:
value = parser.parse(context.scene.DatePickerProperties.selected_date, dayfirst=True, fuzzy=True)
self.set_scene_prop(self.target_prop, helper.canonicalise_time(value))
except:
pass
return {"FINISHED"}
# Just to make sure the date is valid.
tool.Sequence.parse_isodate_datetime(selected_date, self.include_time)
self.set_scene_prop(self.target_prop, selected_date)
return {"FINISHED"}
except Exception as e:
self.report({"ERROR"}, f"Provided date is invalid: '{selected_date}'. Exception: {str(e)}.")
return {"CANCELLED"}
def draw(self, context):
current_date = parser.parse(context.scene.DatePickerProperties.display_date, dayfirst=True, fuzzy=True)
current_month = (current_date.year, current_date.month)
props = context.scene.DatePickerProperties
display_date = tool.Sequence.parse_isodate_datetime(props.display_date, False)
current_month = (display_date.year, display_date.month)
lines = calendar.monthcalendar(*current_month)
month_title, week_titles = calendar.month(*current_month).splitlines()[:2]
layout = self.layout
row = layout.row()
row.prop(context.scene.DatePickerProperties, "selected_date")
row.prop(props, "selected_date", text="Date")
# Time.
if self.include_time:
row = layout.row()
row.label(text="Time:")
row.prop(props, "selected_hour", text="H")
row.prop(props, "selected_min", text="M")
row.prop(props, "selected_sec", text="S")
# Month.
split = layout.split()
col = split.row()
op = col.operator("bim.redraw_datepicker", icon="TRIA_LEFT", text="")
@@ -1210,12 +1228,14 @@ class Bonsai_DatePicker(bpy.types.Operator):
op = col.operator("bim.redraw_datepicker", icon="TRIA_RIGHT", text="")
op.action = "next"
# Day of week.
row = layout.row(align=True)
for title in week_titles.split():
col = row.column(align=True)
col.alignment = "CENTER"
col.label(text=title.strip())
# Days calendar.
for line in lines:
row = layout.row(align=True)
for i in line:
@@ -1223,15 +1243,31 @@ class Bonsai_DatePicker(bpy.types.Operator):
if i == 0:
col.label(text=" ")
else:
selected_date = datetime(year=display_date.year, month=display_date.month, day=i)
op = col.operator("bim.datepicker_setdate", text="{:2d}".format(i))
selected_date = "{}/{}/{}".format(i, current_date.month, current_date.year)
selected_date = parser.parse(selected_date, dayfirst=True, fuzzy=True)
op.selected_date = helper.canonicalise_time(selected_date)
if self.include_time:
selected_date = selected_date.replace(
hour=props.selected_hour, minute=props.selected_min, second=props.selected_sec
)
op.selected_date = tool.Sequence.isodate_datetime(selected_date, self.include_time)
def invoke(self, context, event):
self.display_date = self.get_scene_prop(self.target_prop) or helper.canonicalise_time(datetime.now())
context.scene.DatePickerProperties.display_date = self.display_date
context.scene.DatePickerProperties.selected_date = self.display_date
props = context.scene.DatePickerProperties
current_date_str = self.get_scene_prop(self.target_prop)
if current_date_str:
current_date = tool.Sequence.parse_isodate_datetime(current_date_str, self.include_time)
else:
current_date = datetime.now()
# Seconds of the moment when datepicker opened will probably only annoy users.
current_date = current_date.replace(second=0)
if self.include_time:
props["selected_hour"] = current_date.hour
props["selected_min"] = current_date.minute
props["selected_sec"] = current_date.second
props.display_date = tool.Sequence.isodate_datetime(current_date.replace(day=1), False)
props.selected_date = tool.Sequence.isodate_datetime(current_date, self.include_time)
return context.window_manager.invoke_props_dialog(self)
def get_scene_prop(self, prop_path: str) -> str:
@@ -1261,15 +1297,15 @@ class Bonsai_RedrawDatePicker(bpy.types.Operator):
action: bpy.props.StringProperty()
def invoke(self, context, event):
current_date = parser.parse(context.scene.DatePickerProperties.display_date, dayfirst=True, fuzzy=True)
props = context.scene.DatePickerProperties
current_date = tool.Sequence.parse_isodate_datetime(props.display_date, False)
if self.action == "previous":
date_to_set = current_date - relativedelta.relativedelta(months=1)
elif self.action == "next":
else: # "next".
date_to_set = current_date + relativedelta.relativedelta(months=1)
context.scene.DatePickerProperties.display_date = helper.canonicalise_time(date_to_set)
props.display_date = tool.Sequence.isodate_datetime(date_to_set, False)
return {"FINISHED"}
+15 -1
View File
@@ -520,9 +520,23 @@ class BIMWorkCalendarProperties(PropertyGroup):
end_time: StringProperty(name="End Time")
def update_selected_date(self: "DatePickerProperties", context: bpy.types.Context) -> None:
# `include_time` is `True`, otherwise time props are not displayed in UI.
include_time = True
selected_date = tool.Sequence.parse_isodate_datetime(self.selected_date, include_time)
selected_date = selected_date.replace(hour=self.selected_hour, minute=self.selected_min, second=self.selected_sec)
self.selected_date = tool.Sequence.isodate_datetime(selected_date, include_time)
class DatePickerProperties(PropertyGroup):
display_date: StringProperty(name="Display Date")
display_date: StringProperty(
name="Display Date",
description="Needed to keep track of what month is currently opened in date picker without affecting the currently selected date.",
)
selected_date: StringProperty(name="Selected Date")
selected_hour: IntProperty(min=0, max=23, update=update_selected_date)
selected_min: IntProperty(min=0, max=59, update=update_selected_date)
selected_sec: IntProperty(min=0, max=59, update=update_selected_date)
class BIMDateTextProperties(PropertyGroup):
+37 -13
View File
@@ -16,6 +16,7 @@
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
from __future__ import annotations
import os
import re
import bpy
@@ -24,7 +25,9 @@ import base64
import pystache
import mathutils
import webbrowser
import isodate
import ifcopenshell
import ifcopenshell.ifcopenshell_wrapper as W
import ifcopenshell.util.sequence
import ifcopenshell.util.date
import ifcopenshell.util.element
@@ -34,7 +37,11 @@ import bonsai.tool as tool
import bonsai.bim.helper
from dateutil import parser
from datetime import datetime
from typing import Optional, Any, Union, Literal
from datetime import time as datetime_time
from typing import Optional, Any, Union, Literal, TYPE_CHECKING
if TYPE_CHECKING:
import bonsai.bim.prop
class Sequence(bonsai.core.tool.Sequence):
@@ -344,19 +351,23 @@ class Sequence(bonsai.core.tool.Sequence):
def load_task_time_attributes(cls, task_time: ifcopenshell.entity_instance) -> None:
import bonsai.bim.module.sequence.helper as helper
def callback(name, prop, data):
def callback(
name: str, prop: Union[bonsai.bim.prop.Attribute, None], data: dict[str, Any]
) -> Union[bool, None]:
if prop and prop.data_type == "string":
duration_props = bpy.context.scene.BIMWorkScheduleProperties.durations_attributes.add()
duration_props.name = name
if prop.is_null:
for key in duration_props.keys():
if key != "name":
setattr(duration_props, key, 0)
return True
if name in ["ScheduleDuration", "ActualDuration", "FreeFloat", "TotalFloat"] and data[name]:
for key, value in helper.parse_duration_as_blender_props(data[name]).items():
duration_props[key] = value
return True
# TODO: Check actual attribute type instead of providing attribute names.
if name in ("ScheduleDuration", "ActualDuration", "FreeFloat", "TotalFloat"):
duration_props = bpy.context.scene.BIMWorkScheduleProperties.durations_attributes.add()
duration_props.name = name
if prop.is_null:
for key in duration_props.keys():
if key != "name":
setattr(duration_props, key, 0)
return True
if data[name]:
for key, value in helper.parse_duration_as_blender_props(data[name]).items():
duration_props[key] = value
return True
if isinstance(data[name], datetime):
prop.string_value = "" if prop.is_null else data[name].isoformat()
return True
@@ -1713,3 +1724,16 @@ class Sequence(bonsai.core.tool.Sequence):
def get_animation_color_scheme(cls):
if len(bpy.context.scene.BIMAnimationProperties.saved_color_schemes) > 0:
return tool.Ifc.get().by_id(int(bpy.context.scene.BIMAnimationProperties.saved_color_schemes))
@classmethod
def parse_isodate_datetime(cls, datetime_str: str, include_time: bool) -> datetime:
if include_time:
return isodate.parse_datetime(datetime_str)
date = isodate.parse_date(datetime_str)
return datetime.combine(date, datetime_time())
@classmethod
def isodate_datetime(cls, datetime_: datetime, include_time: bool) -> str:
if include_time:
return isodate.datetime_isoformat(datetime_)
return isodate.date_isoformat(datetime_)