2
#***************************************************************************
3
#* Copyright (c) 2015 Yorik van Havre <yorik@uncreated.net> *
5
#* This program is free software; you can redistribute it and/or modify *
6
#* it under the terms of the GNU Lesser General Public License (LGPL) *
7
#* as published by the Free Software Foundation; either version 2 of *
8
#* the License, or (at your option) any later version. *
9
#* for detail see the LICENCE text file. *
11
#* This program is distributed in the hope that it will be useful, *
12
#* but WITHOUT ANY WARRANTY; without even the implied warranty of *
13
#* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
14
#* GNU Library General Public License for more details. *
16
#* You should have received a copy of the GNU Library General Public *
17
#* License along with this program; if not, write to the Free Software *
18
#* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
21
#***************************************************************************
24
from draftutils import params
28
from PySide import QtCore, QtGui
29
from draftutils.translate import translate
30
from PySide.QtCore import QT_TRANSLATE_NOOP
33
def translate(ctxt,txt):
35
def QT_TRANSLATE_NOOP(ctxt,txt):
39
## @package ArchSchedule
41
# \brief The Schedule object and tools
43
# This module provides tools to build Schedule objects.
44
# Schedules are objects that can count and gather information
45
# about objects in the document, and fill a spreadsheet with the result
47
__title__ = "Arch Schedule"
48
__author__ = "Yorik van Havre"
49
__url__ = "https://www.freecad.org"
52
verbose = True # change this for silent recomputes
56
class _ArchScheduleDocObserver:
58
"doc observer to monitor all recomputes"
60
# https://forum.freecad.org/viewtopic.php?style=3&p=553377#p553377
62
def __init__(self, doc, schedule):
64
self.schedule = schedule
66
def slotRecomputedDocument(self, doc):
70
self.schedule.Proxy.execute(self.schedule)
77
"the Arch Schedule object"
79
def __init__(self,obj):
81
self.setProperties(obj)
83
self.Type = "Schedule"
85
def onDocumentRestored(self,obj):
87
self.setProperties(obj)
88
if hasattr(obj, "Result"):
89
self.update_properties_0v21(obj)
91
def update_properties_0v21(self,obj):
94
self.setSchedulePropertySpreadsheet(sp, obj)
95
obj.removeProperty("Result")
96
from draftutils.messages import _wrn
97
_wrn("v0.21, " + obj.Label + ", " + translate("Arch", "removed property 'Result', and added property 'AutoUpdate'"))
99
_wrn("v0.21, " + sp.Label + ", " + translate("Arch", "added property 'Schedule'"))
101
def setProperties(self,obj):
103
if not "Description" in obj.PropertiesList:
104
obj.addProperty("App::PropertyStringList","Description", "Arch",QT_TRANSLATE_NOOP("App::Property","The description column"))
105
if not "Value" in obj.PropertiesList:
106
obj.addProperty("App::PropertyStringList","Value", "Arch",QT_TRANSLATE_NOOP("App::Property","The values column"))
107
if not "Unit" in obj.PropertiesList:
108
obj.addProperty("App::PropertyStringList","Unit", "Arch",QT_TRANSLATE_NOOP("App::Property","The units column"))
109
if not "Objects" in obj.PropertiesList:
110
obj.addProperty("App::PropertyStringList","Objects", "Arch",QT_TRANSLATE_NOOP("App::Property","The objects column"))
111
if not "Filter" in obj.PropertiesList:
112
obj.addProperty("App::PropertyStringList","Filter", "Arch",QT_TRANSLATE_NOOP("App::Property","The filter column"))
113
if not "CreateSpreadsheet" in obj.PropertiesList:
114
obj.addProperty("App::PropertyBool", "CreateSpreadsheet", "Arch",QT_TRANSLATE_NOOP("App::Property","If True, a spreadsheet containing the results is recreated when needed"))
115
if not "DetailedResults" in obj.PropertiesList:
116
obj.addProperty("App::PropertyBool", "DetailedResults", "Arch",QT_TRANSLATE_NOOP("App::Property","If True, additional lines with each individual object are added to the results"))
117
if not "AutoUpdate" in obj.PropertiesList:
118
obj.addProperty("App::PropertyBool", "AutoUpdate", "Arch",QT_TRANSLATE_NOOP("App::Property","If True, the schedule and the associated spreadsheet are updated whenever the document is recomputed"))
119
obj.AutoUpdate = True
121
# To add the doc observer:
122
self.onChanged(obj,"AutoUpdate")
124
def setSchedulePropertySpreadsheet(self, sp, obj):
125
if not hasattr(sp, "Schedule"):
130
QT_TRANSLATE_NOOP("App::Property", "The BIM Schedule that uses this spreadsheet"))
133
def getSpreadSheet(self, obj, force=False):
135
"""Get the spreadsheet and store it in self.spreadsheet.
137
If force is True the spreadsheet is created if required.
139
try: # Required as self.spreadsheet may get deleted.
140
if getattr(self, "spreadsheet", None) is not None \
141
and getattr(self.spreadsheet, "Schedule", None) == obj:
142
return self.spreadsheet
146
for o in FreeCAD.ActiveDocument.Objects:
147
if o.TypeId == "Spreadsheet::Sheet" \
148
and getattr(o, "Schedule", None) == obj:
150
return self.spreadsheet
152
self.spreadsheet = FreeCAD.ActiveDocument.addObject("Spreadsheet::Sheet", "Result")
153
self.setSchedulePropertySpreadsheet(self.spreadsheet, obj)
154
return self.spreadsheet
158
def onChanged(self,obj,prop):
160
if prop == "CreateSpreadsheet":
161
if obj.CreateSpreadsheet:
162
self.getSpreadSheet(obj, force=True)
164
sp = self.getSpreadSheet(obj)
166
FreeCAD.ActiveDocument.removeObject(sp.Name)
167
self.spreadsheet = None
168
elif prop == "AutoUpdate":
170
if getattr(self, "docObserver", None) is None:
171
self.docObserver = _ArchScheduleDocObserver(FreeCAD.ActiveDocument, obj)
172
FreeCAD.addDocumentObserver(self.docObserver)
173
elif getattr(self, "docObserver", None) is not None:
174
FreeCAD.removeDocumentObserver(self.docObserver)
175
self.docObserver = None
177
def setSpreadsheetData(self,obj,force=False):
179
"""Fills a spreadsheet with the stored data"""
181
if not hasattr(self,"data"):
183
if not hasattr(self,"data"):
187
if not (obj.CreateSpreadsheet or force):
189
sp = self.getSpreadSheet(obj, force=True)
191
# clearAll removes the custom property, we need to re-add it:
192
self.setSchedulePropertySpreadsheet(sp, obj)
194
sp.set("A1","Description")
197
sp.setStyle('A1:C1', 'bold', 'add')
199
for k,v in self.data.items():
203
sp.purgeTouched() # Remove the confusing blue checkmark from the spreadsheet.
204
for o in sp.InList: # Also recompute TechDraw views.
205
o.TypeId == "TechDraw::DrawViewSpreadsheet"
208
def execute(self,obj):
212
if not obj.Description:
213
# empty description column
215
for p in [obj.Value,obj.Unit,obj.Objects,obj.Filter]:
216
# different number of items in each column
217
if len(obj.Description) != len(p):
220
self.data = {} # store all results in self.data, so it lives even without spreadsheet
221
li = 1 # row index - starts at 2 to leave 2 blank rows for the title
223
for i in range(len(obj.Description)):
225
if not obj.Description[i]:
229
self.data["A"+str(li)] = obj.Description[i]
231
l= "OPERATION: "+obj.Description[i]
236
# build set of valid objects
238
objs = obj.Objects[i]
243
objs = objs.split(";")
244
objs = [FreeCAD.ActiveDocument.getObject(o) for o in objs]
245
objs = [o for o in objs if o is not None]
247
objs = FreeCAD.ActiveDocument.Objects
249
# remove object itself if the object is a group
250
if objs[0].isDerivedFrom("App::DocumentObjectGroup"):
252
objs = Draft.get_group_contents(objs)
253
objs = Arch.pruneIncluded(objs,strict=True)
254
# Remove all schedules and spreadsheets:
255
objs = [o for o in objs if Draft.get_type(o) not in ["Schedule", "Spreadsheet::Sheet"]]
260
props = [p.upper() for p in o.PropertiesList]
262
for f in obj.Filter[i].split(";"):
263
args = [a.strip() for a in f.strip().split(":")]
264
if args[0][0] == "!":
266
prop = args[0][1:].upper()
269
prop = args[0].upper()
270
fval = args[1].upper()
275
csprop = o.PropertiesList[props.index(prop)]
276
if fval in getattr(o,csprop).upper():
279
if not (prop in props):
282
csprop = o.PropertiesList[props.index(prop)]
283
if not (fval in getattr(o,csprop).upper()):
289
# perform operation: count or retrieve property
291
if val.upper() == "COUNT":
294
print (val, ",".join([o.Label for o in objs]))
295
self.data["B"+str(li)] = str(val)
296
if obj.DetailedResults:
297
# additional blank line...
299
self.data["A"+str(li)] = " "
301
vals = val.split(".")
302
if vals[0][0].islower():
303
# old-style: first member is not a property
313
unit = unit.replace("^","") # get rid of existing power symbol
314
unit = unit.replace("2","^2")
315
unit = unit.replace("3","^3")
316
unit = unit.replace("²","^2")
317
unit = unit.replace("³","^3")
319
tp = FreeCAD.Units.Area
321
tp = FreeCAD.Units.Volume
323
tp = FreeCAD.Units.Angle
325
tp = FreeCAD.Units.Length
328
dv = params.get_param("Decimals",path="Units")
329
fs = "{:."+str(dv)+"f}" # format string
332
l = o.Name+" ("+o.Label+"):"
333
print (l+(40-len(l))*" ",end="")
338
if hasattr(d,"Value"):
341
FreeCAD.Console.PrintWarning(translate("Arch","Unable to retrieve value from object")+": "+o.Name+"."+".".join(vals)+"\n")
345
v = fs.format(FreeCAD.Units.Quantity(d,tp).getValueAs(unit).Value)
349
if obj.DetailedResults:
351
self.data["A"+str(li)] = o.Name+" ("+o.Label+")"
353
q = FreeCAD.Units.Quantity(d,tp)
354
self.data["B"+str(li)] = str(q.getValueAs(unit).Value)
355
self.data["C"+str(li)] = unit
357
self.data["B"+str(li)] = str(d)
365
q = FreeCAD.Units.Quantity(val,tp)
368
if obj.DetailedResults:
370
self.data["A"+str(li)] = "TOTAL"
372
self.data["B"+str(li)] = str(q.getValueAs(unit).Value)
373
self.data["C"+str(li)] = unit
375
self.data["B"+str(li)] = str(val)
378
v = fs.format(FreeCAD.Units.Quantity(val,tp).getValueAs(unit).Value)
379
print("TOTAL:"+34*" "+v+" "+unit)
382
print("TOTAL:"+34*" "+v)
383
self.setSpreadsheetData(obj)
389
def loads(self,state):
395
class _ViewProviderArchSchedule:
397
"A View Provider for Schedules"
399
def __init__(self,vobj):
403
if self.Object.AutoUpdate is False:
405
return ":/icons/TechDraw_TreePageUnsync.svg"
407
return ":/icons/Arch_Schedule.svg"
412
def attach(self, vobj):
413
self.Object = vobj.Object
415
def setEdit(self, vobj, mode=0):
419
self.taskd = ArchScheduleTaskPanel(vobj.Object)
422
def unsetEdit(self, vobj, mode):
428
def doubleClicked(self, vobj):
431
def setupContextMenu(self, vobj, menu):
432
actionEdit = QtGui.QAction(translate("Arch", "Edit"),
434
QtCore.QObject.connect(actionEdit,
435
QtCore.SIGNAL("triggered()"),
437
menu.addAction(actionEdit)
439
if self.Object.CreateSpreadsheet is True:
440
msg = translate("Arch", "Remove spreadsheet")
442
msg = translate("Arch", "Attach spreadsheet")
443
actionToggleSpreadsheet = QtGui.QAction(QtGui.QIcon(":/icons/Arch_Schedule.svg"),
446
QtCore.QObject.connect(actionToggleSpreadsheet,
447
QtCore.SIGNAL("triggered()"),
448
self.toggleSpreadsheet)
449
menu.addAction(actionToggleSpreadsheet)
452
FreeCADGui.ActiveDocument.setEdit(self.Object, 0)
454
def toggleSpreadsheet(self):
455
self.Object.CreateSpreadsheet = not self.Object.CreateSpreadsheet
457
def claimChildren(self):
458
if hasattr(self,"Object"):
459
return [self.Object.Proxy.getSpreadSheet(self.Object)]
464
def loads(self,state):
467
def getDisplayModes(self,vobj):
470
def getDefaultDisplayMode(self):
473
def setDisplayMode(self,mode):
477
class ArchScheduleTaskPanel:
479
'''The editmode TaskPanel for Schedules'''
481
def __init__(self,obj=None):
483
"""Sets the panel up"""
486
self.form = FreeCADGui.PySideUic.loadUi(":/ui/ArchSchedule.ui")
487
self.form.setWindowIcon(QtGui.QIcon(":/icons/Arch_Schedule.svg"))
490
self.form.buttonAdd.setIcon(QtGui.QIcon(":/icons/list-add.svg"))
491
self.form.buttonDel.setIcon(QtGui.QIcon(":/icons/list-remove.svg"))
492
self.form.buttonClear.setIcon(QtGui.QIcon(":/icons/delete.svg"))
493
self.form.buttonImport.setIcon(QtGui.QIcon(":/icons/document-open.svg"))
494
self.form.buttonExport.setIcon(QtGui.QIcon(":/icons/document-save.svg"))
495
self.form.buttonSelect.setIcon(QtGui.QIcon(":/icons/edit-select-all.svg"))
498
self.form.list.setColumnWidth(0,params.get_param_arch("ScheduleColumnWidth0"))
499
self.form.list.setColumnWidth(1,params.get_param_arch("ScheduleColumnWidth1"))
500
self.form.list.setColumnWidth(2,params.get_param_arch("ScheduleColumnWidth2"))
501
self.form.list.setColumnWidth(3,params.get_param_arch("ScheduleColumnWidth3"))
502
w = params.get_param_arch("ScheduleDialogWidth")
503
h = params.get_param_arch("ScheduleDialogHeight")
504
self.form.resize(w,h)
506
# set delegate - Not using custom delegates for now...
507
#self.form.list.setItemDelegate(ScheduleDelegate())
508
#self.form.list.setEditTriggers(QtGui.QAbstractItemView.DoubleClicked)
511
QtCore.QObject.connect(self.form.buttonAdd, QtCore.SIGNAL("clicked()"), self.add)
512
QtCore.QObject.connect(self.form.buttonDel, QtCore.SIGNAL("clicked()"), self.remove)
513
QtCore.QObject.connect(self.form.buttonClear, QtCore.SIGNAL("clicked()"), self.clear)
514
QtCore.QObject.connect(self.form.buttonImport, QtCore.SIGNAL("clicked()"), self.importCSV)
515
QtCore.QObject.connect(self.form.buttonExport, QtCore.SIGNAL("clicked()"), self.export)
516
QtCore.QObject.connect(self.form.buttonSelect, QtCore.SIGNAL("clicked()"), self.select)
517
QtCore.QObject.connect(self.form.buttonBox, QtCore.SIGNAL("accepted()"), self.accept)
518
QtCore.QObject.connect(self.form.buttonBox, QtCore.SIGNAL("rejected()"), self.reject)
519
QtCore.QObject.connect(self.form, QtCore.SIGNAL("rejected()"), self.reject)
520
self.form.list.clearContents()
523
for p in [obj.Value,obj.Unit,obj.Objects,obj.Filter]:
524
if len(obj.Description) != len(p):
526
self.form.list.setRowCount(len(obj.Description))
528
for j in range(len(obj.Description)):
529
item = QtGui.QTableWidgetItem([obj.Description,obj.Value,obj.Unit,obj.Objects,obj.Filter][i][j])
530
self.form.list.setItem(j,i,item)
531
self.form.lineEditName.setText(self.obj.Label)
532
self.form.checkDetailed.setChecked(self.obj.DetailedResults)
533
self.form.checkSpreadsheet.setChecked(self.obj.CreateSpreadsheet)
535
# center over FreeCAD window
536
mw = FreeCADGui.getMainWindow()
537
self.form.move(mw.frameGeometry().topLeft() + mw.rect().center() - self.form.rect().center())
539
# maintain above FreeCAD window
540
self.form.setWindowFlags(self.form.windowFlags() | QtCore.Qt.WindowStaysOnTopHint)
546
"""Adds a new row below the last one"""
548
self.form.list.insertRow(self.form.list.currentRow()+1)
552
"""Removes the current row"""
554
if self.form.list.currentRow() >= 0:
555
self.form.list.removeRow(self.form.list.currentRow())
559
"""Clears the list"""
561
self.form.list.clearContents()
562
self.form.list.setRowCount(0)
566
"""Imports a CSV file"""
568
filename = QtGui.QFileDialog.getOpenFileName(QtGui.QApplication.activeWindow(), translate("Arch","Import CSV file"), None, "CSV files (*.csv *.CSV)")
570
filename = filename[0]
571
self.form.list.clearContents()
573
with open(filename,'r') as csvfile:
575
for row in csv.reader(csvfile):
576
self.form.list.insertRow(r)
580
#t = t.replace("²","^2")
581
#t = t.replace("³","^3")
582
self.form.list.setItem(r,i,QtGui.QTableWidgetItem(t))
587
"""Exports the results as MD or CSV"""
589
# commit latest changes
593
if not("Up-to-date" in self.obj.State):
594
self.obj.Proxy.execute(self.obj)
595
if not hasattr(self.obj.Proxy,"data"):
597
if not self.obj.Proxy.data:
600
filename = QtGui.QFileDialog.getSaveFileName(QtGui.QApplication.activeWindow(),
601
translate("Arch","Export CSV file"),
603
"Comma-separated values (*.csv);;TAB-separated values (*.tsv);;Markdown (*.md)");
606
filename = filename[0]
607
# add missing extension
608
if (not filename.lower().endswith(".csv")) and (not filename.lower().endswith(".tsv")) and (not filename.lower().endswith(".md")):
615
if filename.lower().endswith(".csv"):
616
self.exportCSV(filename,delimiter=",")
617
elif filename.lower().endswith(".tsv"):
618
self.exportCSV(filename,delimiter="\t")
619
elif filename.lower().endswith(".md"):
620
self.exportMD(filename)
622
FreeCAD.Console.PrintError(translate("Arch","Unable to recognize that file type")+":"+filename+"\n")
626
"""get the rows that contain data"""
629
if hasattr(self.obj.Proxy,"data") and self.obj.Proxy.data:
630
for key in self.obj.Proxy.data.keys():
637
def exportCSV(self,filename,delimiter="\t"):
639
"""Exports the results as a CSV/TSV file"""
642
with open(filename, 'w') as csvfile:
643
csvfile = csv.writer(csvfile,delimiter=delimiter)
644
csvfile.writerow([translate("Arch","Description"),translate("Arch","Value"),translate("Arch","Unit")])
645
if self.obj.DetailedResults:
646
csvfile.writerow(["","",""])
647
for i in self.getRows():
649
for j in ["A","B","C"]:
650
if j+i in self.obj.Proxy.data:
651
r.append(str(self.obj.Proxy.data[j+i]))
655
print("successfully exported ",filename)
657
def exportMD(self,filename):
659
"""Exports the results as a Markdown file"""
661
with open(filename, 'w') as mdfile:
662
mdfile.write("| "+translate("Arch","Description")+" | "+translate("Arch","Value")+" | "+translate("Arch","Unit")+" |\n")
663
mdfile.write("| --- | --- | --- |\n")
664
if self.obj.DetailedResults:
665
mdfile.write("| | | |\n")
666
for i in self.getRows():
668
for j in ["A","B","C"]:
669
if j+i in self.obj.Proxy.data:
670
r.append(str(self.obj.Proxy.data[j+i]))
673
mdfile.write("| "+" | ".join(r)+" |\n")
674
print("successfully exported ",filename)
678
"""Adds selected objects to current row"""
680
if self.form.list.currentRow() >= 0:
682
for o in FreeCADGui.Selection.getSelection():
688
self.form.list.setItem(self.form.list.currentRow(),3,QtGui.QTableWidgetItem(sel))
692
"""Saves the changes and closes the dialog"""
695
params.set_param_arch("ScheduleColumnWidth0",self.form.list.columnWidth(0))
696
params.set_param_arch("ScheduleColumnWidth1",self.form.list.columnWidth(1))
697
params.set_param_arch("ScheduleColumnWidth2",self.form.list.columnWidth(2))
698
params.set_param_arch("ScheduleColumnWidth3",self.form.list.columnWidth(3))
699
params.set_param_arch("ScheduleDialogWidth",self.form.width())
700
params.set_param_arch("ScheduleDialogHeight",self.form.height())
705
FreeCADGui.ActiveDocument.resetEdit()
710
"""Close dialog without saving"""
713
FreeCADGui.ActiveDocument.resetEdit()
716
def writeValues(self):
718
"""commits values and recalculate"""
721
self.obj = FreeCAD.ActiveDocument.addObject("App::FeaturePython","Schedule")
722
self.obj.Label = translate("Arch","Schedule")
723
_ArchSchedule(self.obj)
725
_ViewProviderArchSchedule(self.obj.ViewObject)
726
if hasattr(self.obj,"CreateSpreadsheet") and self.obj.CreateSpreadsheet:
727
self.obj.Proxy.getSpreadSheet(self.obj, force=True)
728
lists = [ [], [], [], [], [] ]
729
for i in range(self.form.list.rowCount()):
731
cell = self.form.list.item(i,j)
733
lists[j].append(cell.text())
736
FreeCAD.ActiveDocument.openTransaction("Edited Schedule")
737
self.obj.Description = lists[0]
738
self.obj.Value = lists[1]
739
self.obj.Unit = lists[2]
740
self.obj.Objects = lists[3]
741
self.obj.Filter = lists[4]
742
self.obj.Label = self.form.lineEditName.text()
743
self.obj.DetailedResults = self.form.checkDetailed.isChecked()
744
self.obj.CreateSpreadsheet = self.form.checkSpreadsheet.isChecked()
745
self.obj.AutoUpdate = self.form.checkAutoUpdate.isChecked()
746
FreeCAD.ActiveDocument.commitTransaction()
747
FreeCAD.ActiveDocument.recompute()