Files
create/src/Mod/BIM/ArchSchedule.py
Roy-043 8a13cbba67 BIM+Draft: change object related version info to log messages
To inform the user warnings are shown if an object has new or modified properties in the current version. These warnings can however be confusing, especially if there are many. With this PR they are turned into log messages. They are also moved out of translation, and instead of the object Label the object Name is displayed.

Additionally:
Zero path length warnings for path arrays are now only displayed if the Align property is True.

See: #21180.
2025-09-08 10:58:28 +02:00

1006 lines
38 KiB
Python

# SPDX-License-Identifier: LGPL-2.1-or-later
# ***************************************************************************
# * *
# * Copyright (c) 2015 Yorik van Havre <yorik@uncreated.net> *
# * *
# * 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 *
# * <https://www.gnu.org/licenses/>. *
# * *
# ***************************************************************************
__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()
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()