From d4b055aeb269ea785d2c03fd88bc36b0d9cec46d Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 28 Jun 2024 15:33:34 +0500 Subject: [PATCH] Open recent IFC project #3657 Example - https://imgur.com/a/EDfBTJS Paths to the opened IFC projects are now stored and then you can open them from File -> Open Recent IFC Project menu. It's done in very similar way to how Blender does it, list of files is stored in `Blender\4.1\config\recent-ifc-projects.txt`. --- .../blenderbim/bim/module/project/__init__.py | 2 + .../blenderbim/bim/module/project/operator.py | 56 ++++++++++++++++- .../blenderbim/bim/module/project/ui.py | 25 ++++++++ src/blenderbim/blenderbim/tool/blender.py | 63 +++++++++++++++++++ 4 files changed, 145 insertions(+), 1 deletion(-) diff --git a/src/blenderbim/blenderbim/bim/module/project/__init__.py b/src/blenderbim/blenderbim/bim/module/project/__init__.py index b6dc0a4f3f..479faaaf77 100644 --- a/src/blenderbim/blenderbim/bim/module/project/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/project/__init__.py @@ -30,6 +30,7 @@ classes = ( operator.ChangeLibraryElement, operator.CreateClippingPlane, operator.CreateProject, + operator.ClearRecentIFCProjects, operator.DisableCulling, operator.DisableEditingHeader, operator.EditHeader, @@ -66,6 +67,7 @@ classes = ( ui.BIM_MT_new_project, ui.BIM_MT_project, ui.BIM_PT_new_project_wizard, + ui.BIM_MT_recent_projects, ui.BIM_PT_project, ui.BIM_PT_project_library, ui.BIM_PT_links, diff --git a/src/blenderbim/blenderbim/bim/module/project/operator.py b/src/blenderbim/blenderbim/bim/module/project/operator.py index a6d67a6baa..2f4876af24 100644 --- a/src/blenderbim/blenderbim/bim/module/project/operator.py +++ b/src/blenderbim/blenderbim/bim/module/project/operator.py @@ -24,6 +24,7 @@ import logging import tempfile import traceback import subprocess +import datetime import numpy as np import ifcopenshell import ifcopenshell.api @@ -655,6 +656,46 @@ class LoadProject(bpy.types.Operator, IFCFileSelector): is_advanced: bpy.props.BoolProperty(name="Enable Advanced Mode", default=False) use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=False) should_start_fresh_session: bpy.props.BoolProperty(name="Should Start Fresh Session", default=True) + use_detailed_tooltip: bpy.props.BoolProperty(default=False, options={"HIDDEN"}) + + @classmethod + def description(cls, context, properties): + tooltip = cls.bl_description + if not properties.use_detailed_tooltip: + return tooltip + + filepath = properties.filepath + if not filepath: + return tooltip + filepath = Path(filepath) + tooltip += f".\n\n" + if not filepath.exists(): + tooltip += "File does not exist" + return tooltip + + def get_modified_date(st_mtime: float) -> str: + mod_time = datetime.datetime.fromtimestamp(st_mtime) + + today = datetime.date.today() + if mod_time.date() == today: + return f"Today {mod_time.strftime('%I:%M %p')}" + elif mod_time.date() == today - datetime.timedelta(days=1): + return f"Yesterday {mod_time.strftime('%I:%M %p')}" + return mod_time.strftime("%d %b %Y") + + def get_file_size(size_bytes: float) -> str: + if size_bytes < 1024 * 1024: # Less than 1 MiB + size = size_bytes / 1024 + return f"{size:.0f} KiB" + else: + size = size_bytes / (1024 * 1024) + return f"{size:.1f} MiB" + + file_stat = filepath.stat() + tooltip += f"Modified: {get_modified_date(file_stat.st_mtime)}" + tooltip += f"\nSize: {get_file_size(file_stat.st_size)}" + + return tooltip def execute(self, context): @persistent @@ -683,10 +724,13 @@ class LoadProject(bpy.types.Operator, IFCFileSelector): for obj in bpy.data.objects: bpy.data.objects.remove(obj) - context.scene.BIMProperties.ifc_file = self.get_filepath() + filepath = Path(self.get_filepath()) + context.scene.BIMProperties.ifc_file = str(filepath) context.scene.BIMProjectProperties.is_loading = True context.scene.BIMProjectProperties.total_elements = len(tool.Ifc.get().by_type("IfcElement")) tool.Blender.register_toolbar() + tool.Blender.add_recent_ifc_project(filepath.absolute()) + if not self.is_advanced: bpy.ops.bim.load_project_elements() except: @@ -703,6 +747,16 @@ class LoadProject(bpy.types.Operator, IFCFileSelector): IFCFileSelector.draw(self, context) +class ClearRecentIFCProjects(bpy.types.Operator): + bl_idname = "bim.clear_recent_ifc_projects" + bl_label = "Clear Recent IFC Projects List" + bl_options = {"REGISTER"} + + def execute(self, context): + tool.Blender.clear_recent_ifc_projects() + return {"FINISHED"} + + class RevertProject(bpy.types.Operator, IFCFileSelector): bl_idname = "bim.revert_project" bl_label = "Revert IFC Project" diff --git a/src/blenderbim/blenderbim/bim/module/project/ui.py b/src/blenderbim/blenderbim/bim/module/project/ui.py index 81ba87db0e..c6fabef9e1 100644 --- a/src/blenderbim/blenderbim/bim/module/project/ui.py +++ b/src/blenderbim/blenderbim/bim/module/project/ui.py @@ -18,6 +18,7 @@ import os import blenderbim.bim +import blenderbim.tool as tool from blenderbim.bim.helper import prop_with_search from bpy.types import Panel, Menu, UIList from blenderbim.bim.ifc import IfcStore @@ -36,6 +37,28 @@ class BIM_MT_project(Menu): self.layout.operator("bim.new_project", text="New Project Wizard").preset = "wizard" +class BIM_MT_recent_projects(Menu): + bl_idname = "BIM_MT_recent_projects" + bl_label = "Open Recent IFC Project" + + def draw(self, context): + paths = tool.Blender.get_recent_ifc_projects() + if not paths: + self.layout.label(text="No Recent IFC Projects") + return + + for path in paths: + op = self.layout.operator( + "bim.load_project", text=path.name, icon_value=blenderbim.bim.icons["IFC"].icon_id + ) + op.filepath = str(path) + op.should_start_fresh_session = True + op.use_detailed_tooltip = True + + self.layout.separator() + self.layout.operator("bim.clear_recent_ifc_projects", icon="TRASH") + + class BIM_MT_new_project(Menu): bl_idname = "BIM_MT_new_project" bl_label = "New Project" @@ -44,6 +67,7 @@ class BIM_MT_new_project(Menu): self.layout.operator_context = "INVOKE_DEFAULT" op = self.layout.operator("bim.load_project", text="Open IFC Project", icon="FILEBROWSER") op.should_start_fresh_session = True + self.layout.menu("BIM_MT_recent_projects", icon="NONE") # Do we need to set it back to exec default? # self.layout.operator_context = "EXEC_DEFAULT" self.layout.separator() @@ -67,6 +91,7 @@ def file_menu(self, context): self.layout.menu("BIM_MT_project", icon="COLLECTION_NEW") op = self.layout.operator("bim.load_project", text="Open IFC Project", icon="FILEBROWSER") op.should_start_fresh_session = True + self.layout.menu("BIM_MT_recent_projects", icon="NONE") self.layout.separator() op = self.layout.operator("bim.save_project", icon="FILE_TICK", text="Save IFC Project") op.should_save_as = False diff --git a/src/blenderbim/blenderbim/tool/blender.py b/src/blenderbim/blenderbim/tool/blender.py index 4dc2c7244d..40fd25c494 100644 --- a/src/blenderbim/blenderbim/tool/blender.py +++ b/src/blenderbim/blenderbim/tool/blender.py @@ -19,6 +19,7 @@ import bpy import bmesh import json +import os import ifcopenshell.api import ifcopenshell.util.element import blenderbim.core.tool @@ -1069,3 +1070,65 @@ class Blender(blenderbim.core.tool.Blender): bpy.utils.unregister_class(panel) bpy.utils.register_class(panel) del polls[panel] + + @classmethod + def get_recent_ifc_projects_path(cls) -> Path: + return Path(bpy.utils.user_resource("CONFIG")) / "recent-ifc-projects.txt" + + _recent_ifc_projects_loaded: bool = False + _recent_ifc_projects: list[Path] = [] + + @classmethod + def get_recent_ifc_projects(cls) -> list[Path]: + if cls._recent_ifc_projects_loaded: + return cls._recent_ifc_projects + + filepath = cls.get_recent_ifc_projects_path() + if not filepath.exists(): + cls._recent_ifc_projects = [] + return [] + + paths = [] + with open(filepath, "r") as fi: + for line in fi: + line = line.strip() + if not line: + continue + paths.append(Path(line)) + + cls._recent_ifc_projects = paths + return paths + + @classmethod + def write_recent_ifc_projects(cls, filepaths: list[Path]) -> None: + recent_projects_path = cls.get_recent_ifc_projects_path() + try: + recent_projects_path.parent.mkdir(parents=True, exist_ok=True) + with open(recent_projects_path, "w") as fo: + fo.write("\n".join(str(p) for p in filepaths)) + cls._recent_ifc_projects_loaded = False + except PermissionError: + msg = ( + f"WARNING. PermissionError trying to access '{str(recent_projects_path)}'. " + "List of recently opened IFC projects won't be stored between Blender sessions." + ) + print(msg) + cls._recent_ifc_projects = filepaths + + @classmethod + def add_recent_ifc_project(cls, filepath: Path) -> None: + """Add `filepath` to the list of the recently opened IFC projects. + + If `filepath` was opened before, bump it in the list. + """ + current_filepaths = cls.get_recent_ifc_projects() + if filepath in current_filepaths: + current_filepaths.remove(filepath) + current_filepaths = [filepath] + current_filepaths + # Limit it to 20 recent files. + current_filepaths = current_filepaths[:20] + cls.write_recent_ifc_projects(current_filepaths) + + @classmethod + def clear_recent_ifc_projects(cls) -> None: + cls.write_recent_ifc_projects([])