# SPDX-License-Identifier: LGPL-2.1-or-later # *************************************************************************** # * * # * Copyright (c) 2015 Yorik van Havre * # * * # * This file is part of FreeCAD. * # * * # * FreeCAD is free software: you can redistribute it and/or modify it * # * under the terms of the GNU Lesser General Public License as * # * published by the Free Software Foundation, either version 2.1 of the * # * License, or (at your option) any later version. * # * * # * FreeCAD is distributed in the hope that it will be useful, but * # * WITHOUT ANY WARRANTY; without even the implied warranty of * # * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * # * Lesser General Public License for more details. * # * * # * You should have received a copy of the GNU Lesser General Public * # * License along with FreeCAD. If not, see * # * . * # * * # *************************************************************************** __title__ = "Arch Schedule" __author__ = "Yorik van Havre" __url__ = "https://www.freecad.org" ## @package ArchSchedule # \ingroup ARCH # \brief The Schedule object and tools # # This module provides tools to build Schedule objects. # Schedules are objects that can count and gather information # about objects in the document, and fill a spreadsheet with the result import FreeCAD from draftutils import params if FreeCAD.GuiUp: from PySide import QtCore, QtGui from PySide.QtCore import QT_TRANSLATE_NOOP import FreeCADGui from draftutils.translate import translate else: # \cond def translate(ctxt, txt): return txt def QT_TRANSLATE_NOOP(ctxt, txt): return txt # \endcond PARAMS = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/BIM") VERBOSE = True # change this for silent recomputes class _ArchScheduleDocObserver: "doc observer to monitor all recomputes" # https://forum.freecad.org/viewtopic.php?style=3&p=553377#p553377 def __init__(self, doc, schedule): self.doc = doc self.schedule = schedule def slotRecomputedDocument(self, doc): if doc != self.doc: return try: self.schedule.Proxy.execute(self.schedule) except: pass class _ArchSchedule: "the Arch Schedule object" def __init__(self, obj): self.setProperties(obj) obj.Proxy = self self.Type = "Schedule" def onDocumentRestored(self, obj): self.setProperties(obj) if hasattr(obj, "Result"): self.update_properties_0v21(obj) if hasattr(obj, "Description"): self.update_properties_1v1(obj) def update_properties_0v21(self, obj): from draftutils.messages import _log sp = obj.Result if sp is not None: self.setSchedulePropertySpreadsheet(sp, obj) obj.removeProperty("Result") _log("v0.21, " + obj.Name + ", removed property 'Result', and added property 'AutoUpdate'") if sp is not None: _log("v0.21, " + sp.Name + ", added property 'Schedule'") def update_properties_1v1(self, obj): from draftutils.messages import _log if obj.getTypeIdOfProperty("Description") == "App::PropertyStringList": obj.Operation = obj.Description obj.removeProperty("Description") _log("v1.1, " + obj.Name + ", renamed property 'Description' to 'Operation'") for prop in ( "Operation", "Value", "Unit", "Objects", "Filter", "CreateSpreadsheet", "DetailedResults", ): obj.setGroupOfProperty(prop, "Schedule") def setProperties(self, obj): if not "Operation" in obj.PropertiesList: obj.addProperty( "App::PropertyStringList", "Operation", "Schedule", QT_TRANSLATE_NOOP("App::Property", "The operation column"), locked=True, ) if not "Value" in obj.PropertiesList: obj.addProperty( "App::PropertyStringList", "Value", "Schedule", QT_TRANSLATE_NOOP("App::Property", "The values column"), locked=True, ) if not "Unit" in obj.PropertiesList: obj.addProperty( "App::PropertyStringList", "Unit", "Schedule", QT_TRANSLATE_NOOP("App::Property", "The units column"), locked=True, ) if not "Objects" in obj.PropertiesList: obj.addProperty( "App::PropertyStringList", "Objects", "Schedule", QT_TRANSLATE_NOOP("App::Property", "The objects column"), locked=True, ) if not "Filter" in obj.PropertiesList: obj.addProperty( "App::PropertyStringList", "Filter", "Schedule", QT_TRANSLATE_NOOP("App::Property", "The filter column"), locked=True, ) if not "CreateSpreadsheet" in obj.PropertiesList: obj.addProperty( "App::PropertyBool", "CreateSpreadsheet", "Schedule", QT_TRANSLATE_NOOP( "App::Property", "If True, a spreadsheet containing the results is recreated when needed", ), locked=True, ) if not "DetailedResults" in obj.PropertiesList: obj.addProperty( "App::PropertyBool", "DetailedResults", "Schedule", QT_TRANSLATE_NOOP( "App::Property", "If True, additional lines with each individual object are added to the results", ), locked=True, ) if not "AutoUpdate" in obj.PropertiesList: obj.addProperty( "App::PropertyBool", "AutoUpdate", "Schedule", QT_TRANSLATE_NOOP( "App::Property", "If True, the schedule and the associated spreadsheet are updated whenever the document is recomputed", ), locked=True, ) obj.AutoUpdate = True # To add the doc observer: self.onChanged(obj, "AutoUpdate") def setSchedulePropertySpreadsheet(self, sp, obj): if not hasattr(sp, "Schedule"): sp.addProperty( "App::PropertyLink", "Schedule", "Arch", QT_TRANSLATE_NOOP("App::Property", "The BIM Schedule that uses this spreadsheet"), locked=True, ) sp.Schedule = obj def getSpreadSheet(self, obj, force=False): """Get the spreadsheet and store it in self.spreadsheet. If force is True the spreadsheet is created if required. """ try: # Required as self.spreadsheet may get deleted. if ( getattr(self, "spreadsheet", None) is not None and getattr(self.spreadsheet, "Schedule", None) == obj ): return self.spreadsheet except: pass else: for o in FreeCAD.ActiveDocument.Objects: if o.TypeId == "Spreadsheet::Sheet" and getattr(o, "Schedule", None) == obj: self.spreadsheet = o return self.spreadsheet if force: self.spreadsheet = FreeCAD.ActiveDocument.addObject("Spreadsheet::Sheet", "Result") self.setSchedulePropertySpreadsheet(self.spreadsheet, obj) return self.spreadsheet else: return None def onChanged(self, obj, prop): if prop == "CreateSpreadsheet": if obj.CreateSpreadsheet: self.getSpreadSheet(obj, force=True) else: sp = self.getSpreadSheet(obj) if sp is not None: FreeCAD.ActiveDocument.removeObject(sp.Name) self.spreadsheet = None elif prop == "AutoUpdate": if obj.AutoUpdate: if getattr(self, "docObserver", None) is None: self.docObserver = _ArchScheduleDocObserver(FreeCAD.ActiveDocument, obj) FreeCAD.addDocumentObserver(self.docObserver) elif getattr(self, "docObserver", None) is not None: FreeCAD.removeDocumentObserver(self.docObserver) self.docObserver = None def setSpreadsheetData(self, obj, force=False): """Fills a spreadsheet with the stored data""" if not hasattr(self, "data"): self.execute(obj) if not hasattr(self, "data"): return if not self.data: return if not (obj.CreateSpreadsheet or force): return sp = self.getSpreadSheet(obj, force=True) widths = [sp.getColumnWidth(col) for col in ("A", "B", "C")] sp.clearAll() # clearAll resets the column widths: for col, width in zip(("A", "B", "C"), widths): sp.setColumnWidth(col, width) # set headers sp.set("A1", "Operation") sp.set("B1", "Value") sp.set("C1", "Unit") sp.setStyle("A1:C1", "bold", "add") # write contents for k, v in self.data.items(): sp.set(k, v) # recompute sp.recompute() sp.purgeTouched() # Remove the confusing blue checkmark from the spreadsheet. for o in sp.InList: # Also recompute TechDraw views. o.TypeId == "TechDraw::DrawViewSpreadsheet" o.recompute() def execute(self, obj): # verify the data if not obj.Operation: # empty description column return for p in [obj.Value, obj.Unit, obj.Objects, obj.Filter]: # different number of items in each column if len(obj.Operation) != len(p): return self.data = {} # store all results in self.data, so it lives even without spreadsheet self.li = 1 # row index - starts at 2 to leave 2 blank rows for the title for i in range(len(obj.Operation)): self.li += 1 if not obj.Operation[i]: # blank line continue # write description self.data["A" + str(self.li)] = obj.Operation[i] if VERBOSE: l = "OPERATION: " + obj.Operation[i] print("") print(l) print(len(l) * "=") # build set of valid objects objs = obj.Objects[i] val = obj.Value[i] unit = obj.Unit[i] details = obj.DetailedResults ifcfile = None elts = None if val: import Draft import Arch if objs: objs = objs.split(";") objs = [FreeCAD.ActiveDocument.getObject(o) for o in objs] objs = [o for o in objs if o is not None] else: if hasattr(getattr(FreeCAD.ActiveDocument, "Proxy", None), "ifcfile"): ifcfile = FreeCAD.ActiveDocument.Proxy.ifcfile objs = FreeCAD.ActiveDocument.Objects if len(objs) == 1: if hasattr(objs[0], "StepId"): from nativeifc import ifc_tools ifcfile = ifc_tools.get_ifcfile(objs[0]) # remove object itself if the object is a group if objs[0].isDerivedFrom("App::DocumentObjectGroup"): objs = objs[0].Group objs = Draft.get_group_contents(objs) objs = self.expandArrays(objs) # Remove included objects (e.g. walls that are part of another wall, # base geometry, etc) objs = Arch.pruneIncluded(objs, strict=True, silent=True) # Remove all schedules and spreadsheets: objs = [ o for o in objs if Draft.get_type(o) not in ["Schedule", "Spreadsheet::Sheet"] ] # filter elements if obj.Filter[i]: if ifcfile: elts = self.get_ifc_elements(ifcfile, obj.Filter[i]) else: objs = self.apply_filter(objs, obj.Filter[i]) # perform operation: count or retrieve property if ifcfile: if elts: self.update_from_elts(elts, val, unit, details) elif objs: self.update_from_objs(objs, val, unit, details) self.setSpreadsheetData(obj) self.save_ifc_props(obj) def apply_filter(self, objs, filters): """Applies the given filters to the given list of objects""" nobjs = [] for o in objs: props = [p.upper() for p in o.PropertiesList] ok = True for f in filters.split(";"): args = [a.strip() for a in f.strip().split(":")] if args[0][0] == "!": inv = True prop = args[0][1:].upper() else: inv = False prop = args[0].upper() fval = args[1].upper() if prop == "TYPE": prop = "IFCTYPE" if inv: if prop in props: csprop = o.PropertiesList[props.index(prop)] if fval in getattr(o, csprop).upper(): ok = False else: if not (prop in props): ok = False else: csprop = o.PropertiesList[props.index(prop)] if not (fval in getattr(o, csprop).upper()): ok = False if ok: nobjs.append(o) return nobjs def get_ifc_elements(self, ifcfile, filters): """Retrieves IFC elements corresponding to the given filters""" elts = [] for el in ifcfile.by_type("IfcProduct"): ok = True for f in filters.split(";"): args = [a.strip() for a in f.strip().split(":")] if args[0][0] == "!": inv = True prop = args[0][1:] else: inv = False prop = args[0] fval = args[1] if prop.upper() in ["CLASS", "IFCCLASS", "IFCTYPE"]: prop = "is_a" if inv: if prop == "is_a": if not fval.upper().startswith("IFC"): fval = "Ifc" + fval fval = fval.replace(" ", "") if el.is_a(fval): ok = False else: if prop in dir(el): rval = getattr(el, prop) if hasattr(rval, "id"): if fval.startswith("#"): fval = int(fval[1:]) if rval == fval: ok = False else: if prop == "is_a": if not fval.upper().startswith("IFC"): fval = "Ifc" + fval fval = fval.replace(" ", "") if not el.is_a(fval): ok = False else: if prop in dir(el): rval = getattr(el, prop) if hasattr(rval, "id"): if fval.startswith("#"): fval = int(fval[1:]) if rval != fval: ok = False else: ok = False if ok: elts.append(el) return elts def update_from_objs(self, objs, val, unit, details): """Updates the spreadsheet data from FreeCAD objects""" if val.upper() == "COUNT": val = len(objs) if VERBOSE: print(val, ",".join([o.Label for o in objs])) self.data["B" + str(self.li)] = str(val) if details: # additional blank line... self.li += 1 self.data["A" + str(self.li)] = " " else: vals = val.split(".") if vals[0][0].islower(): # old-style: first member is not a property vals = vals[1:] sumval = 0 # get unit tp = None unit = None q = None if unit: unit = unit.replace("^", "") # get rid of existing power symbol unit = unit.replace("2", "^2") unit = unit.replace("3", "^3") unit = unit.replace("²", "^2") unit = unit.replace("³", "^3") if "2" in unit: tp = FreeCAD.Units.Area elif "3" in unit: tp = FreeCAD.Units.Volume elif "deg" in unit: tp = FreeCAD.Units.Angle else: tp = FreeCAD.Units.Length # format value dv = params.get_param("Decimals", path="Units") fs = "{:." + str(dv) + "f}" # format string for o in objs: if VERBOSE: l = o.Name + " (" + o.Label + "):" print(l + (40 - len(l)) * " ", end="") try: d = o for v in vals: d = getattr(d, v) if hasattr(d, "Value"): d = d.Value except Exception: t = translate("Arch", "Unable to retrieve value from object") FreeCAD.Console.PrintWarning(t + ": " + o.Name + "." + ".".join(vals) + "\n") else: if VERBOSE: if tp and unit: v = fs.format(FreeCAD.Units.Quantity(d, tp).getValueAs(unit).Value) print(v, unit) elif isinstance(d, str): if d.replace(".", "", 1).isdigit(): print(fs.format(d)) else: print(d) else: print(fs.format(d)) if details: self.li += 1 self.data["A" + str(self.li)] = o.Name + " (" + o.Label + ")" if tp and unit: q = FreeCAD.Units.Quantity(d, tp) self.data["B" + str(self.li)] = str(q.getValueAs(unit).Value) self.data["C" + str(self.li)] = unit else: self.data["B" + str(self.li)] = str(d) if sumval: sumval += d else: sumval = d val = sumval if tp: q = FreeCAD.Units.Quantity(val, tp) # write data if details: self.li += 1 self.data["A" + str(self.li)] = "TOTAL" if q and unit: self.data["B" + str(self.li)] = str(q.getValueAs(unit).Value) self.data["C" + str(self.li)] = unit else: self.data["B" + str(self.li)] = str(val) if VERBOSE: if tp and unit: v = fs.format(FreeCAD.Units.Quantity(val, tp).getValueAs(unit).Value) print("TOTAL:" + 34 * " " + v + " " + unit) elif isinstance(val, str): if val.replace(".", "", 1).isdigit(): v = fs.format(val) print("TOTAL:" + 34 * " " + v) else: print("TOTAL:" + 34 * " " + val) else: v = fs.format(val) print("TOTAL:" + 34 * " " + v) def update_from_elts(self, elts, val, unit, details): """Updates the spreadsheet data from IFC elements""" if val.upper() == "COUNT": val = len(elts) if VERBOSE: print("COUNT:", val, "(", ",".join(["#" + str(e.id()) for e in elts]), ")") self.data["B" + str(self.li)] = str(val) if details: # additional blank line... self.li += 1 self.data["A" + str(self.li)] = " " else: total = 0 for el in elts: if val in dir(el): elval = getattr(el, val, "") if isinstance(elval, tuple): if len(elval) == 1: elval = elval[0] elif len(elval) == 0: elval = "" if hasattr(elval, "is_a") and elval.is_a("IfcRelationship"): for att in dir(elval): if att.startswith("Relating"): targ = getattr(elval, att) if targ != el: elval = targ break elif att.startswith("Related"): if not elval in getattr(elval, att): elval = str(getattr(elval, att)) break if details: self.li += 1 name = el.Name if el.Name else "" self.data["A" + str(self.li)] = "#" + str(el.id()) + name self.data["B" + str(self.li)] = str(elval) if VERBOSE: print("#" + str(el.id()) + "." + val + " = " + str(elval)) if isinstance(elval, str) and elval.replace(".", "", 1).isdigit(): total += float(elval) elif isinstance(elval, (int, float)): total += elval if total: if details: self.li += 1 self.data["A" + str(self.li)] = "TOTAL" self.data["B" + str(self.li)] = str(total) if VERBOSE: print("TOTAL:", str(total)) def create_ifc(self, obj, ifcfile, export=False): """Creates an IFC element for this object""" from nativeifc import ifc_tools # lazy loading proj = ifcfile.by_type("IfcProject")[0] elt = ifc_tools.api_run("root.create_entity", ifcfile, ifc_class="IfcControl") ifc_tools.set_attribute(ifcfile, elt, "Name", obj.Label) ifc_tools.api_run( "project.assign_declaration", ifcfile, definitions=[elt], relating_context=proj ) if not export: ifc_tools.add_properties(obj, ifcfile, elt) return elt def save_ifc_props(self, obj, ifcfile=None, elt=None): """Saves the object data to IFC""" from nativeifc import ifc_psets # lazy loading ifc_psets.edit_pset( obj, "Operation", "::".join(obj.Operation), ifcfile=ifcfile, element=elt ) ifc_psets.edit_pset(obj, "Value", "::".join(obj.Value), ifcfile=ifcfile, element=elt) ifc_psets.edit_pset(obj, "Unit", "::".join(obj.Unit), ifcfile=ifcfile, element=elt) ifc_psets.edit_pset(obj, "Objects", "::".join(obj.Objects), ifcfile=ifcfile, element=elt) ifc_psets.edit_pset(obj, "Filter", "::".join(obj.Filter), ifcfile=ifcfile, element=elt) def export_ifc(self, obj, ifcfile): """Exports the object to IFC (does not modify the FreeCAD object).""" elt = self.create_ifc(obj, ifcfile, export=True) self.save_ifc_props(obj, ifcfile, elt) return elt def dumps(self): return self.Type def loads(self, state): if state: self.Type = state def getIfcClass(self, obj): """gets the IFC class of this object""" if hasattr(obj, "IfcType"): return obj.IfcType elif hasattr(obj, "IfcRole"): return obj.IfcRole elif hasattr(obj, "IfcClass"): return obj.IfcClass else: return None def getArray(self, obj): "returns a count number if this object needs to be duplicated" import Draft elementCount = 0 # The given object can belong to multiple arrays # o is a potential parent array of the given object for o in obj.InList: if Draft.getType(o) == "Array": elementCount += o.Count return elementCount def expandArrays(self, objs): """Expands array elements in the given list of objects""" expandedobjs = [] for obj in objs: ifcClass = self.getIfcClass(obj) # This filters out the array object itself, which has no IFC class, # but leaves the array elements, which do have an IFC class. if ifcClass: expandedobjs.append(obj) # If the object is in an array, add it and the rest of its elements # to the list. array = self.getArray(obj) for i in range(1, array): # The first element (0) was already added expandedobjs.append(obj) return expandedobjs class _ViewProviderArchSchedule: "A View Provider for Schedules" def __init__(self, vobj): vobj.Proxy = self def getIcon(self): if self.Object.AutoUpdate is False: import TechDrawGui return ":/icons/TechDraw_TreePageUnsync.svg" import Arch_rc return ":/icons/Arch_Schedule.svg" def isShow(self): return True def attach(self, vobj): self.Object = vobj.Object def setEdit(self, vobj, mode=0): if mode != 0: return None self.taskd = ArchScheduleTaskPanel(vobj.Object) if not self.taskd.form.isVisible(): from PySide import QtCore QtCore.QTimer.singleShot(100, self.showEditor) return True def showEditor(self): if hasattr(self, "taskd"): self.taskd.form.show() def unsetEdit(self, vobj, mode): if mode != 0: return None return True def doubleClicked(self, vobj): self.edit() return True def setupContextMenu(self, vobj, menu): if FreeCADGui.activeWorkbench().name() != "BIMWorkbench": return actionEdit = QtGui.QAction(translate("Arch", "Edit"), menu) QtCore.QObject.connect(actionEdit, QtCore.SIGNAL("triggered()"), self.edit) menu.addAction(actionEdit) if self.Object.CreateSpreadsheet is True: msg = translate("Arch", "Remove spreadsheet") else: msg = translate("Arch", "Attach spreadsheet") actionToggleSpreadsheet = QtGui.QAction(QtGui.QIcon(":/icons/Arch_Schedule.svg"), msg, menu) QtCore.QObject.connect( actionToggleSpreadsheet, QtCore.SIGNAL("triggered()"), self.toggleSpreadsheet ) menu.addAction(actionToggleSpreadsheet) def edit(self): FreeCADGui.ActiveDocument.setEdit(self.Object, 0) def toggleSpreadsheet(self): self.Object.CreateSpreadsheet = not self.Object.CreateSpreadsheet def claimChildren(self): if hasattr(self, "Object"): return [self.Object.Proxy.getSpreadSheet(self.Object)] def dumps(self): return None def loads(self, state): return None def getDisplayModes(self, vobj): return ["Default"] def getDefaultDisplayMode(self): return "Default" def setDisplayMode(self, mode): return mode class ArchScheduleTaskPanel: """The editmode TaskPanel for Schedules""" def __init__(self, obj=None): """Sets the panel up""" self.obj = obj self.form = FreeCADGui.PySideUic.loadUi(":/ui/ArchSchedule.ui") self.form.setWindowIcon(QtGui.QIcon(":/icons/Arch_Schedule.svg")) # set icons self.form.buttonAdd.setIcon(QtGui.QIcon(":/icons/list-add.svg")) self.form.buttonDel.setIcon(QtGui.QIcon(":/icons/list-remove.svg")) self.form.buttonClear.setIcon(QtGui.QIcon(":/icons/delete.svg")) self.form.buttonImport.setIcon(QtGui.QIcon(":/icons/document-open.svg")) self.form.buttonExport.setIcon(QtGui.QIcon(":/icons/document-save.svg")) self.form.buttonSelect.setIcon(QtGui.QIcon(":/icons/edit-select-all.svg")) # restore widths self.form.list.setColumnWidth(0, params.get_param_arch("ScheduleColumnWidth0")) self.form.list.setColumnWidth(1, params.get_param_arch("ScheduleColumnWidth1")) self.form.list.setColumnWidth(2, params.get_param_arch("ScheduleColumnWidth2")) self.form.list.setColumnWidth(3, params.get_param_arch("ScheduleColumnWidth3")) w = params.get_param_arch("ScheduleDialogWidth") h = params.get_param_arch("ScheduleDialogHeight") self.form.resize(w, h) # restore default states self.form.checkAutoUpdate.setChecked(PARAMS.GetBool("ScheduleAutoUpdate", False)) # set delegate - Not using custom delegates for now... # self.form.list.setItemDelegate(ScheduleDelegate()) # self.form.list.setEditTriggers(QtGui.QAbstractItemView.DoubleClicked) # connect slots self.form.buttonAdd.clicked.connect(self.add) self.form.buttonDel.clicked.connect(self.remove) self.form.buttonClear.clicked.connect(self.clear) self.form.buttonImport.clicked.connect(self.importCSV) self.form.buttonExport.clicked.connect(self.export) self.form.buttonSelect.clicked.connect(self.select) self.form.buttonBox.accepted.connect(self.accept) self.form.buttonBox.rejected.connect(self.reject) self.form.rejected.connect(self.reject) self.form.list.clearContents() if self.obj: # for p in [obj.Value,obj.Unit,obj.Objects,obj.Filter]: # if len(obj.Operation) != len(p): # return self.form.list.setRowCount(len(obj.Operation)) for i in range(5): for j in range(len(obj.Operation)): try: text = [obj.Operation, obj.Value, obj.Unit, obj.Objects, obj.Filter][i][j] except: text = "" item = QtGui.QTableWidgetItem(text) self.form.list.setItem(j, i, item) self.form.lineEditName.setText(self.obj.Label) self.form.checkSpreadsheet.setChecked(self.obj.CreateSpreadsheet) self.form.checkDetailed.setChecked(self.obj.DetailedResults) self.form.checkAutoUpdate.setChecked(self.obj.AutoUpdate) # center over FreeCAD window mw = FreeCADGui.getMainWindow() self.form.move( mw.frameGeometry().topLeft() + mw.rect().center() - self.form.rect().center() ) self.form.show() def add(self): """Adds a new row below the last one""" self.form.list.insertRow(self.form.list.currentRow() + 1) def remove(self): """Removes the current row""" if self.form.list.currentRow() >= 0: self.form.list.removeRow(self.form.list.currentRow()) def clear(self): """Clears the list""" self.form.list.clearContents() self.form.list.setRowCount(0) def importCSV(self): """Imports a CSV file""" filename = QtGui.QFileDialog.getOpenFileName( QtGui.QApplication.activeWindow(), translate("Arch", "Import CSV file"), None, "CSV files (*.csv *.CSV)", ) if filename: filename = filename[0] self.form.list.clearContents() import csv with open(filename, "r") as csvfile: r = 0 for row in csv.reader(csvfile): self.form.list.insertRow(r) for i in range(5): if len(row) > i: t = row[i] # t = t.replace("²","^2") # t = t.replace("³","^3") self.form.list.setItem(r, i, QtGui.QTableWidgetItem(t)) r += 1 def export(self): """Exports the results as MD or CSV""" # commit latest changes self.writeValues() # tests if not ("Up-to-date" in self.obj.State): self.obj.Proxy.execute(self.obj) if not hasattr(self.obj.Proxy, "data"): return if not self.obj.Proxy.data: return filename = QtGui.QFileDialog.getSaveFileName( QtGui.QApplication.activeWindow(), translate("Arch", "Export CSV file"), None, "Comma-separated values (*.csv);;TAB-separated values (*.tsv);;Markdown (*.md)", ) if filename: filt = filename[1] filename = filename[0] # add missing extension if ( (not filename.lower().endswith(".csv")) and (not filename.lower().endswith(".tsv")) and (not filename.lower().endswith(".md")) ): if "csv" in filt: filename += ".csv" elif "tsv" in filt: filename += ".tsv" else: filename += ".md" if filename.lower().endswith(".csv"): self.exportCSV(filename, delimiter=",") elif filename.lower().endswith(".tsv"): self.exportCSV(filename, delimiter="\t") elif filename.lower().endswith(".md"): self.exportMD(filename) else: FreeCAD.Console.PrintError( translate("Arch", "Unable to recognize that file type") + ":" + filename + "\n" ) def getRows(self): """get the rows that contain data""" rows = [] if hasattr(self.obj.Proxy, "data") and self.obj.Proxy.data: for key in self.obj.Proxy.data.keys(): n = key[1:] if not n in rows: rows.append(n) rows.sort(key=int) return rows def exportCSV(self, filename, delimiter="\t"): """Exports the results as a CSV/TSV file""" import csv with open(filename, "w") as csvfile: csvfile = csv.writer(csvfile, delimiter=delimiter) csvfile.writerow( [ translate("Arch", "Operation"), translate("Arch", "Value"), translate("Arch", "Unit"), ] ) if self.obj.DetailedResults: csvfile.writerow(["", "", ""]) for i in self.getRows(): r = [] for j in ["A", "B", "C"]: if j + i in self.obj.Proxy.data: r.append(str(self.obj.Proxy.data[j + i])) else: r.append("") csvfile.writerow(r) print("successfully exported ", filename) def exportMD(self, filename): """Exports the results as a Markdown file""" with open(filename, "w") as mdfile: mdfile.write( "| " + translate("Arch", "Operation") + " | " + translate("Arch", "Value") + " | " + translate("Arch", "Unit") + " |\n" ) mdfile.write("| --- | --- | --- |\n") if self.obj.DetailedResults: mdfile.write("| | | |\n") for i in self.getRows(): r = [] for j in ["A", "B", "C"]: if j + i in self.obj.Proxy.data: r.append(str(self.obj.Proxy.data[j + i])) else: r.append("") mdfile.write("| " + " | ".join(r) + " |\n") print("successfully exported ", filename) def select(self): """Adds selected objects to current row""" if self.form.list.currentRow() >= 0: sel = "" for o in FreeCADGui.Selection.getSelection(): if o != self.obj: if sel: sel += ";" sel += o.Name if sel: self.form.list.setItem(self.form.list.currentRow(), 3, QtGui.QTableWidgetItem(sel)) def accept(self): """Saves the changes and closes the dialog""" # store widths params.set_param_arch("ScheduleColumnWidth0", self.form.list.columnWidth(0)) params.set_param_arch("ScheduleColumnWidth1", self.form.list.columnWidth(1)) params.set_param_arch("ScheduleColumnWidth2", self.form.list.columnWidth(2)) params.set_param_arch("ScheduleColumnWidth3", self.form.list.columnWidth(3)) params.set_param_arch("ScheduleDialogWidth", self.form.width()) params.set_param_arch("ScheduleDialogHeight", self.form.height()) # store default states PARAMS.SetBool("ScheduleAutoUpdate", self.form.checkAutoUpdate.isChecked()) # commit values self.writeValues() self.form.hide() FreeCADGui.ActiveDocument.resetEdit() return True def reject(self): """Close dialog without saving""" self.form.hide() FreeCADGui.ActiveDocument.resetEdit() return True def writeValues(self): """commits values and recalculate""" if not self.obj: import Arch self.obj = Arch.makeSchedule() lists = [[], [], [], [], []] for i in range(self.form.list.rowCount()): for j in range(5): cell = self.form.list.item(i, j) if cell: lists[j].append(cell.text()) else: lists[j].append("") FreeCAD.ActiveDocument.openTransaction("Edited Schedule") self.obj.Operation = lists[0] self.obj.Value = lists[1] self.obj.Unit = lists[2] self.obj.Objects = lists[3] self.obj.Filter = lists[4] self.obj.Label = self.form.lineEditName.text() self.obj.DetailedResults = self.form.checkDetailed.isChecked() self.obj.CreateSpreadsheet = self.form.checkSpreadsheet.isChecked() self.obj.AutoUpdate = self.form.checkAutoUpdate.isChecked() FreeCAD.ActiveDocument.commitTransaction() FreeCAD.ActiveDocument.recompute()