# -*- coding: utf8 -*- #*************************************************************************** #* * #* Copyright (c) 2015 - Yorik van Havre * #* * #* This program is free software; you can redistribute it and/or modify * #* it under the terms of the GNU Lesser General Public License (LGPL) * #* as published by the Free Software Foundation; either version 2 of * #* the License, or (at your option) any later version. * #* for detail see the LICENCE text file. * #* * #* This program 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 Library General Public License for more details. * #* * #* You should have received a copy of the GNU Library General Public * #* License along with this program; if not, write to the Free Software * #* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * #* USA * #* * #*************************************************************************** import sys import FreeCAD, time if FreeCAD.GuiUp: import FreeCADGui, Arch_rc, os from PySide import QtCore, QtGui from DraftTools import translate from PySide.QtCore import QT_TRANSLATE_NOOP else: # \cond def translate(ctxt,txt): return txt def QT_TRANSLATE_NOOP(ctxt,txt): return txt # \endcond ## @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 __title__ = "Arch Schedule" __author__ = "Yorik van Havre" __url__ = "http://www.freecadweb.org" verbose = True # change this for silent recomputes class CommandArchSchedule: "the Arch Schedule command definition" def GetResources(self): return {'Pixmap': 'Arch_Schedule', 'MenuText': QT_TRANSLATE_NOOP("Arch_Schedule","Schedule"), 'ToolTip': QT_TRANSLATE_NOOP("Arch_Schedule","Creates a schedule to collect data from the model")} def Activated(self): taskd = ArchScheduleTaskPanel() FreeCADGui.Control.showDialog(taskd) def IsActive(self): if FreeCAD.ActiveDocument: return True else: return False 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) def setProperties(self,obj): if not "Description" in obj.PropertiesList: obj.addProperty("App::PropertyStringList","Description", "Arch",QT_TRANSLATE_NOOP("App::Property","The description column")) if not "Value" in obj.PropertiesList: obj.addProperty("App::PropertyStringList","Value", "Arch",QT_TRANSLATE_NOOP("App::Property","The values column")) if not "Unit" in obj.PropertiesList: obj.addProperty("App::PropertyStringList","Unit", "Arch",QT_TRANSLATE_NOOP("App::Property","The units column")) if not "Objects" in obj.PropertiesList: obj.addProperty("App::PropertyStringList","Objects", "Arch",QT_TRANSLATE_NOOP("App::Property","The objects column")) if not "Filter" in obj.PropertiesList: obj.addProperty("App::PropertyStringList","Filter", "Arch",QT_TRANSLATE_NOOP("App::Property","The filter column")) if not "CreateSpreadsheet" in obj.PropertiesList: obj.addProperty("App::PropertyBool", "CreateSpreadsheet", "Arch",QT_TRANSLATE_NOOP("App::Property","If True, a spreadsheet containing the results is recreated when needed")) if not "Result" in obj.PropertiesList: obj.addProperty("App::PropertyLink", "Result", "Arch",QT_TRANSLATE_NOOP("App::Property","The spreadsheet to print the results to")) if not "DetailedResults" in obj.PropertiesList: obj.addProperty("App::PropertyBool", "DetailedResults", "Arch",QT_TRANSLATE_NOOP("App::Property","If True, additional lines with each individual object are added to the results")) def onChanged(self,obj,prop): if (prop == "CreateSpreadsheet"): if hasattr(obj,"CreateSpreadsheet") and obj.CreateSpreadsheet: if not obj.Result: import Spreadsheet sp = FreeCAD.ActiveDocument.addObject("Spreadsheet::Sheet","Result") obj.Result = sp def setSpreadsheetData(self,obj): """Fills a spreadsheet with the stored data""" if not hasattr(self,"data"): return if not self.data: return if not obj.Result: if obj.CreateSpreadsheet: import Spreadsheet sp = FreeCAD.ActiveDocument.addObject("Spreadsheet::Sheet","Result") obj.Result = sp else: return # clear spreadsheet obj.Result.clearAll() # set headers obj.Result.set("A1","Description") obj.Result.set("B1","Value") obj.Result.set("C1","Unit") obj.Result.setStyle('A1:C1', 'bold', 'add') # write contents for k,v in self.data.items(): obj.Result.set(k,v) # recompute obj.Result.recompute() def execute(self,obj): # verify the data if not obj.Description: # 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.Description) != len(p): return if not hasattr(obj,"Result"): # silently fail on old schedule objects return self.data = {} # store all results in self.data, so it lives even without spreadsheet li = 1 # row index - starts at 2 to leave 2 blank rows for the title for i in range(len(obj.Description)): li += 1 if not obj.Description[i]: # blank line continue # write description if sys.version_info.major >= 3: # use unicode for python3 self.data["A"+str(li)] = obj.Description[i] else: self.data["A"+str(li)] = obj.Description[i].encode("utf8") if verbose: l= "OPERATION: "+obj.Description[i] print("") print (l) print (len(l)*"=") # build set of valid objects objs = obj.Objects[i] val = obj.Value[i] if val: import Draft,Arch if objs: objs = objs.split(";") objs = [FreeCAD.ActiveDocument.getObject(o) for o in objs] objs = [o for o in objs if o != None] else: objs = FreeCAD.ActiveDocument.Objects if len(objs) == 1: # remove object itself if the object is a group if objs[0].isDerivedFrom("App::DocumentObjectGroup"): objs = objs[0].Group objs = Draft.getGroupContents(objs) objs = Arch.pruneIncluded(objs,strict=True) # remove the schedule object and its result from the list objs = [o for o in objs if not o == obj] objs = [o for o in objs if not o == obj.Result] if obj.Filter[i]: # apply filters nobjs = [] for o in objs: ok = True for f in obj.Filter[i].split(";"): args = [a.strip() for a in f.strip().split(":")] if args[0].upper() == "NAME": if not(args[1].upper() in o.Name.upper()): ok = False elif args[0].upper() == "!NAME": if (args[1].upper() in o.Name.upper()): ok = False elif args[0].upper() == "LABEL": if not(args[1].upper() in o.Label.upper()): ok = False elif args[0].upper() == "!LABEL": if args[1].upper() in o.Label.upper(): ok = False elif args[0].upper() == "TYPE": if hasattr(o,"IfcType"): if o.IfcType.upper() != args[1].upper(): ok = False else: ok = False elif args[0].upper() == "!TYPE": if hasattr(o,"IfcType"): if o.IfcType.upper() == args[1].upper(): ok = False if ok: nobjs.append(o) objs = nobjs # perform operation: count or retrieve property if val.upper() == "COUNT": val = len(objs) if verbose: print (val, ",".join([o.Label for o in objs])) self.data["B"+str(li)] = str(val) if obj.DetailedResults: # additional blank line... li += 1 self.data["A"+str(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 obj.Unit[i]: unit = obj.Unit[i] if sys.version_info.major < 3: unit = unit.encode("utf8") 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 = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Units").GetInt("Decimals",2) 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 if verbose: if tp and unit: v = fs.format(FreeCAD.Units.Quantity(d,tp).getValueAs(unit).Value) print(v,unit) else: print(fs.format(d)) if obj.DetailedResults: li += 1 self.data["A"+str(li)] = o.Name+" ("+o.Label+")" if tp and unit: q = FreeCAD.Units.Quantity(d,tp) self.data["B"+str(li)] = str(q.getValueAs(unit).Value) self.data["C"+str(li)] = unit else: self.data["B"+str(li)] = str(d) except: FreeCAD.Console.PrintWarning(translate("Arch","Unable to retrieve value from object")+": "+o.Name+"."+".".join(vals)+"\n") else: if not sumval: sumval = d else: sumval += d val = sumval if tp: q = FreeCAD.Units.Quantity(val,tp) # write data if obj.DetailedResults: li += 1 self.data["A"+str(li)] = "TOTAL" if q and unit: self.data["B"+str(li)] = str(q.getValueAs(unit).Value) self.data["C"+str(li)] = unit else: self.data["B"+str(li)] = str(val) if obj.DetailedResults: # additional blank line... li += 1 self.data["A"+str(li)] = " " if verbose: if tp and unit: v = fs.format(FreeCAD.Units.Quantity(val,tp).getValueAs(unit).Value) print("TOTAL:"+34*" "+v+" "+unit) else: v = fs.format(val) print("TOTAL:"+34*" "+v) self.setSpreadsheetData(obj) def __getstate__(self): return self.Type def __setstate__(self,state): if state: self.Type = state class _ViewProviderArchSchedule: "A View Provider for Schedules" def __init__(self,vobj): vobj.Proxy = self def getIcon(self): 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): taskd = ArchScheduleTaskPanel(vobj.Object) FreeCADGui.Control.showDialog(taskd) return True def doubleClicked(self,vobj): taskd = ArchScheduleTaskPanel(vobj.Object) FreeCADGui.Control.showDialog(taskd) return True def unsetEdit(self,vobj,mode): FreeCADGui.Control.closeDialog() return def claimChildren(self): if hasattr(self,"Object"): return [self.Object.Result] def __getstate__(self): return None def __setstate__(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 p = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Arch") self.form.list.setColumnWidth(0,p.GetInt("ScheduleColumnWidth0",100)) self.form.list.setColumnWidth(1,p.GetInt("ScheduleColumnWidth1",100)) self.form.list.setColumnWidth(2,p.GetInt("ScheduleColumnWidth2",50)) self.form.list.setColumnWidth(3,p.GetInt("ScheduleColumnWidth3",100)) # set delegate - Not using custom delegates for now... #self.form.list.setItemDelegate(ScheduleDelegate()) #self.form.list.setEditTriggers(QtGui.QAbstractItemView.DoubleClicked) # connect slots QtCore.QObject.connect(self.form.buttonAdd, QtCore.SIGNAL("clicked()"), self.add) QtCore.QObject.connect(self.form.buttonDel, QtCore.SIGNAL("clicked()"), self.remove) QtCore.QObject.connect(self.form.buttonClear, QtCore.SIGNAL("clicked()"), self.clear) QtCore.QObject.connect(self.form.buttonImport, QtCore.SIGNAL("clicked()"), self.importCSV) QtCore.QObject.connect(self.form.buttonExport, QtCore.SIGNAL("clicked()"), self.export) QtCore.QObject.connect(self.form.buttonSelect, QtCore.SIGNAL("clicked()"), self.select) self.form.list.clearContents() if self.obj: if not obj.Description: return for p in [obj.Value,obj.Unit,obj.Objects,obj.Filter]: if len(obj.Description) != len(p): return self.form.list.setRowCount(len(obj.Description)) for i in range(5): for j in range(len(obj.Description)): item = QtGui.QTableWidgetItem([obj.Description,obj.Value,obj.Unit,obj.Objects,obj.Filter][i][j]) self.form.list.setItem(j,i,item) self.form.lineEditName.setText(self.obj.Label) self.form.checkDetailed.setChecked(self.obj.DetailedResults) self.form.checkSpreadsheet.setChecked(self.obj.CreateSpreadsheet) 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 file (*.csv)"); if filename: filename = filename[0] if sys.version_info.major < 3: filename = filename.encode("utf8") 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, "CSV (*.csv);;Markdown (*.md)"); if filename: filt = filename[1] filename = filename[0] if sys.version_info.major < 3: filename = filename.encode("utf8") # add missing extension if (not filename.lower().endswith(".csv")) and (not filename.lower().endswith(".md")): if "csv" in filt: filename += ".csv" else: filename += ".md" if filename.lower().endswith(".csv"): self.exportCSV(filename) 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): """Exports the results as a CSV file""" # use TAB to separate values DELIMITER = "\t" import csv with open(filename, 'w') as csvfile: csvfile = csv.writer(csvfile,delimiter=DELIMITER) csvfile.writerow([translate("Arch","Description"),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","Description")+" | "+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): """executes when OK button has been pressed""" # store widths p = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Arch") p.SetInt("ScheduleColumnWidth0",self.form.list.columnWidth(0)) p.SetInt("ScheduleColumnWidth1",self.form.list.columnWidth(1)) p.SetInt("ScheduleColumnWidth2",self.form.list.columnWidth(2)) p.SetInt("ScheduleColumnWidth3",self.form.list.columnWidth(3)) # commit values self.writeValues() return True def writeValues(self): """commits values and recalculate""" if not self.obj: self.obj = FreeCAD.ActiveDocument.addObject("App::FeaturePython","Schedule") self.obj.Label = translate("Arch","Schedule") _ArchSchedule(self.obj) if FreeCAD.GuiUp: _ViewProviderArchSchedule(self.obj.ViewObject) if hasattr(self.obj,"CreateSpreadsheet") and self.obj.CreateSpreadsheet: import Spreadsheet sp = FreeCAD.ActiveDocument.addObject("Spreadsheet::Sheet","Result") self.obj.Result = sp 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.Description = 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() FreeCAD.ActiveDocument.commitTransaction() FreeCAD.ActiveDocument.recompute() if FreeCAD.GuiUp: FreeCADGui.addCommand('Arch_Schedule',CommandArchSchedule())