# -*- 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 * #* * #*************************************************************************** from __future__ import print_function import sys import FreeCAD if FreeCAD.GuiUp: import FreeCADGui 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__ = "https://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): if hasattr(self,"taskd"): if self.taskd: self.taskd.form.hide() self.taskd = ArchScheduleTaskPanel() 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,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.Result: if obj.CreateSpreadsheet or force: 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.get_group_contents(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: props = [p.upper() for p in o.PropertiesList] ok = True for f in obj.Filter[i].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) 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 except Exception: FreeCAD.Console.PrintWarning(translate("Arch","Unable to retrieve value from object")+": "+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) 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) 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=0): if hasattr(self,"taskd"): if self.taskd: self.taskd.form.hide() self.taskd = ArchScheduleTaskPanel(vobj.Object) return True def doubleClicked(self,vobj): self.setEdit(vobj) def unsetEdit(self,vobj,mode): return True 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 def setupContextMenu(self,vobj,menu): action1 = QtGui.QAction(QtGui.QIcon(":/icons/Arch_Schedule.svg"),"Attach spreadsheet",menu) QtCore.QObject.connect(action1,QtCore.SIGNAL("triggered()"),self.attachSpreadsheet) menu.addAction(action1) def attachSpreadsheet(self): if hasattr(self,"Object"): self.Object.Proxy.setSpreadsheetData(self.Object,force=True) 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)) w = p.GetInt("ScheduleDialogWidth",300) h = p.GetInt("ScheduleDialogHeight",200) self.form.resize(w,h) # 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) QtCore.QObject.connect(self.form.buttonBox, QtCore.SIGNAL("accepted()"), self.accept) QtCore.QObject.connect(self.form.buttonBox, QtCore.SIGNAL("rejected()"), self.reject) QtCore.QObject.connect(self.form, QtCore.SIGNAL("rejected()"), self.reject) 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) # center over FreeCAD window mw = FreeCADGui.getMainWindow() self.form.move(mw.frameGeometry().topLeft() + mw.rect().center() - self.form.rect().center()) # maintain above FreeCAD window self.form.setWindowFlags(self.form.windowFlags() | QtCore.Qt.WindowStaysOnTopHint) 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 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, "Comma-separated values (*.csv);;TAB-separated values (*.tsv);;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(".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","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): """Saves the changes and closes the dialog""" # 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)) p.SetInt("ScheduleDialogWidth",self.form.width()) p.SetInt("ScheduleDialogHeight",self.form.height()) # commit values self.writeValues() self.form.hide() return True def reject(self): """Close dialog without saving""" self.form.hide() 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())