# -*- coding: utf-8 -*- # *************************************************************************** # * Copyright (c) 2022 Zheng Lei (realthunder) * # * * # * 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 * # * * # *************************************************************************** '''Utilites for generating C++ code for parameters management using Python Cog ''' import cog import inspect, re from os import path def quote(txt, indent=0): lines = [ ' '*indent + '"' + l.replace('\\', '\\\\').replace('"', '\\"') for l in txt.split('\n')] return '\\n"\n'.join(lines) + '"' def init_params(params, namespace, class_name, param_path, header_file=None): for param in params: param.path = param_path if not header_file: header_file = [f'{namespace}/{class_name}.h'] param.header_file = header_file + getattr(param.proxy, 'header_file', []) param.namespace = namespace param.class_name = class_name return params def auto_comment(frame=1, msg=None, count=1): trace = [] for stack in inspect.stack()[frame:frame+count]: filename = path.normpath(stack[1]).split('/src/')[-1] if filename.find('<') >= 0: break lineno = stack[2] trace.insert(0, f'{filename}:{lineno}') return f'{"// Auto generated code" if msg is None else msg} ({" <- ".join(trace)})' def trace_comment(): return auto_comment(2) def get_module_path(module): return path.dirname(module.__file__.split(f'src{path.sep}')[-1]) def declare_begin(module, header=True): class_name = module.ClassName namespace = module.NameSpace params = module.Params param_path = module.ParamPath file_path = getattr(module, 'FilePath', get_module_path(module)) param_file = getattr(module, 'ParamSource', f'{file_path}/{class_name}.py') header_file = getattr(module, 'HeaderFile', f'{file_path}/{class_name}.h') source_file = getattr(module, 'SourceFile', f'{file_path}/{class_name}.cpp') class_doc = module.ClassDoc signal = getattr(module, 'Signal', False) if header: cog.out(f''' {trace_comment()} #include {"#include " if signal else ""} ''') cog.out(f''' {trace_comment()} namespace {namespace} {{ /** {class_doc} * The parameters are under group "{param_path}" * * This class is auto generated by {param_file}. Modify that file * instead of this one, if you want to add any parameter. You need * to install Cog Python package for code generation: * @code * pip install cogapp * @endcode * * Once modified, you can regenerate the header and the source file, * @code * python3 -m cogapp -r {header_file} {source_file} * @endcode * * You can add a new parameter by adding lines in {param_file}. Available * parameter types are 'Int, UInt, String, Bool, Float'. For example, to add * a new Int type parameter, * @code * ParamInt(parameter_name, default_value, documentation, on_change=False) * @endcode * * If there is special handling on parameter change, pass in on_change=True. * And you need to provide a function implementation in {source_file} with * the following signature. * @code * void {class_name}:onChanged() * @endcode */ class {namespace}Export {class_name} {{ public: static ParameterGrp::handle getHandle(); ''') if signal: cog.out(f''' static boost::signals2::signal &signalParamChanged(); static void signalAll(); ''') for param in params: cog.out(f''' {trace_comment()} //@{{ /// Accessor for parameter {param.name}''') if param._doc: cog.out(f''' ///''') for line in param._doc.split('\n'): cog.out(f''' /// {line}''') cog.out(f''' static const {param.C_Type} & get{param.name}(); static const {param.C_Type} & default{param.name}(); static void remove{param.name}(); static void set{param.name}(const {param.C_Type} &v); static const char *doc{param.name}();''') if param.on_change: cog.out(f''' static void on{param.name}Changed();''') cog.out(f''' //@}} ''') def declare_end(module): class_name = module.ClassName namespace = module.NameSpace cog.out(f''' {trace_comment()} }}; // class {class_name} }} // namespace {namespace} ''') def define(module, header=True): class_name = module.ClassName namespace = module.NameSpace params = module.Params param_path = module.ParamPath class_doc = module.ClassDoc signal = getattr(module, 'Signal', False) if header: cog.out(f''' {trace_comment()} #include #include #include #include "{class_name}.h" using namespace {namespace}; ''') cog.out(f''' {trace_comment()} namespace {{ class {class_name}P: public ParameterGrp::ObserverType {{ public: ParameterGrp::handle handle; std::unordered_map funcs; ''') if signal: cog.out(f''' {trace_comment()} boost::signals2::signal signalParamChanged; void signalAll() {{''') for param in params: cog.out(f''' signalParamChanged("{param.name}");''') cog.out(f''' {trace_comment()} }}''') for param in params: cog.out(f''' {param.C_Type} {param.name};''') cog.out(f''' {trace_comment()} {class_name}P() {{ handle = App::GetApplication().GetParameterGroupByPath("{param_path}"); handle->Attach(this); ''') for param in params: cog.out(f''' {param.name} = {param.getter('handle')}; funcs["{param.name}"] = &{class_name}P::update{param.name};''') cog.out(f''' }} {trace_comment()} ~{class_name}P() {{ }} ''') cog.out(f''' {trace_comment()} void OnChange(Base::Subject &, const char* sReason) {{ if(!sReason) return; auto it = funcs.find(sReason); if(it == funcs.end()) return; it->second(this); {"signalParamChanged(sReason);" if signal else ""} }} ''') for param in params: if not param.on_change: cog.out(f''' {trace_comment()} static void update{param.name}({class_name}P *self) {{ self->{param.name} = {param.getter('self->handle')}; }}''') else: cog.out(f''' {trace_comment()} static void update{param.name}({class_name}P *self) {{ auto v = {param.getter('self->handle')}; if (self->{param.name} != v) {{ self->{param.name} = v; {class_name}::on{param.name}Changed(); }} }}''') cog.out(f''' }}; {trace_comment()} {class_name}P *instance() {{ static {class_name}P *inst = new {class_name}P; return inst; }} }} // Anonymous namespace ''') cog.out(f''' {trace_comment()} ParameterGrp::handle {class_name}::getHandle() {{ return instance()->handle; }} ''') if signal: cog.out(f''' {trace_comment()} boost::signals2::signal & {class_name}::signalParamChanged() {{ return instance()->signalParamChanged; }} ''') cog.out(f''' {trace_comment()} void signalAll() {{ instance()->signalAll(); }} ''') for param in params: cog.out(f''' {trace_comment()} const char *{class_name}::doc{param.name}() {{ return {param.doc(class_name)}; }} ''') cog.out(f''' {trace_comment()} const {param.C_Type} & {class_name}::get{param.name}() {{ return instance()->{param.name}; }} ''') cog.out(f''' {trace_comment()} const {param.C_Type} & {class_name}::default{param.name}() {{ const static {param.C_Type} def = {param.default}; return def; }} ''') cog.out(f''' {trace_comment()} void {class_name}::set{param.name}(const {param.C_Type} &v) {{ {param.setter()}; instance()->{param.name} = v; }} ''') cog.out(f''' {trace_comment()} void {class_name}::remove{param.name}() {{ instance()->handle->Remove{param.Type}("{param.name}"); }} ''') def widgets_declare(param_set): param_group = param_set.ParamGroup for title,params in param_group: name = _regex.sub('', title) cog.out(f''' {trace_comment()} QGroupBox * group{name} = nullptr;''') for param in params: param.declare_widget() def widgets_init(param_set): param_group = param_set.ParamGroup cog.out(f''' auto layout = new QVBoxLayout(this);''') for title, params in param_group: name = _regex.sub('', title) cog.out(f''' {trace_comment()} group{name} = new QGroupBox(this); layout->addWidget(group{name}); auto layoutHoriz{name} = new QHBoxLayout(group{name}); auto layout{name} = new QGridLayout(); layoutHoriz{name}->addLayout(layout{name}); layoutHoriz{name}->addStretch();''') for row,param in enumerate(params): cog.out(f''' {trace_comment()}''') param.init_widget(row, name) cog.out(''' layout->addItem(new QSpacerItem(40, 20, QSizePolicy::Fixed, QSizePolicy::Expanding)); retranslateUi();''') def widgets_restore(param_set): param_group = param_set.ParamGroup cog.out(f''' {trace_comment()}''') for _,params in param_group: for param in params: param.widget_restore() def widgets_save(param_set): param_group = param_set.ParamGroup cog.out(f''' {trace_comment()}''') for _,params in param_group: for param in params: param.widget_save() def preference_dialog_declare_begin(param_set, header=True): namespace = param_set.NameSpace class_name = param_set.ClassName dialog_namespace = getattr(param_set, 'DialogNameSpace', 'Dialog') param_group = param_set.ParamGroup file_path = getattr(param_set, 'FilePath', get_module_path(param_set)) param_file = getattr(param_set, 'ParamSource', f'{file_path}/{class_name}.py') header_file = getattr(param_set, 'HeaderFile', f'{file_path}/{class_name}.h') source_file = getattr(param_set, 'SourceFile', f'{file_path}/{class_name}.cpp') class_doc = param_set.ClassDoc if header: cog.out(f''' {trace_comment()} #include #include ''') cog.out(f''' {trace_comment()} class QLabel; class QGroupBox; namespace {namespace} {{ namespace {dialog_namespace} {{ /** {class_doc} * This class is auto generated by {param_file}. Modify that file * instead of this one, if you want to make any change. You need * to install Cog Python package for code generation: * @code * pip install cogapp * @endcode * * Once modified, you can regenerate the header and the source file, * @code * python3 -m cogapp -r {header_file} {source_file} * @endcode */ class {class_name} : public Gui::Dialog::PreferencePage {{ Q_OBJECT public: {class_name}( QWidget* parent = 0 ); ~{class_name}(); void saveSettings(); void loadSettings(); void retranslateUi(); protected: void changeEvent(QEvent *e); private:''') widgets_declare(param_set) def preference_dialog_declare_end(param_set): class_name = param_set.ClassName namespace = param_set.NameSpace dialog_namespace = getattr(param_set, 'DialogNameSpace', 'Dialog') cog.out(f''' {trace_comment()} }}; }} // namespace {dialog_namespace} }} // namespace {namespace} ''') def preference_dialog_declare(param_set, header=True): preference_dialog_declare_begin(param_set, header) preference_dialog_declare_end(param_set) _regex = re.compile(r"[^a-zA-Z_]") def preference_dialog_define(param_set, header=True): param_group = param_set.ParamGroup class_name = param_set.ClassName dialog_namespace = getattr(param_set, 'DialogNameSpace', 'Dialog') namespace = f'{param_set.NameSpace}::{dialog_namespace}' file_path = getattr(param_set, 'FilePath', get_module_path(param_set)) param_file = getattr(param_set, 'ParamSource', f'{file_path}/{class_name}.py') header_file = getattr(param_set, 'HeaderFile', f'{file_path}/{class_name}.h') source_file = getattr(param_set, 'SourceFile', f'{file_path}/{class_name}.cpp') user_init = getattr(param_set, 'UserInit', '') headers = set() if header: cog.out(f''' {trace_comment()} #ifndef _PreComp_ # include # include # include # include # include # include #endif''') for _,params in param_group: for param in params: for header in param.header_file: if header not in headers: headers.add(header) cog.out(f''' #include <{header}>''') cog.out(f''' {trace_comment()} #include "{header_file}" using namespace {namespace}; /* TRANSLATOR {namespace}::{class_name} */ ''') cog.out(f''' {trace_comment()} {class_name}::{class_name}(QWidget* parent) : PreferencePage( parent ) {{ ''') widgets_init(param_set) cog.out(f''' {trace_comment()} {user_init} }} ''') cog.out(f''' {trace_comment()} {class_name}::~{class_name}() {{ }} ''') cog.out(f''' {trace_comment()} void {class_name}::saveSettings() {{''') widgets_save(param_set) cog.out(f''' }} {trace_comment()} void {class_name}::loadSettings() {{''') widgets_restore(param_set) cog.out(f''' }} {trace_comment()} void {class_name}::retranslateUi() {{ setWindowTitle(QObject::tr("{param_set.Title}"));''') for title, params in param_group: name = _regex.sub('', title) cog.out(f''' group{name}->setTitle(QObject::tr("{title}"));''') for row,param in enumerate(params): param.retranslate() cog.out(f''' }} {trace_comment()} void {class_name}::changeEvent(QEvent *e) {{ if (e->type() == QEvent::LanguageChange) {{ retranslateUi(); }} QWidget::changeEvent(e); }} ''') cog.out(f''' {trace_comment()} #include "moc_{class_name}.cpp" ''') _ParamPrefix = 'User parameter:BaseApp/Preferences/' class Param: WidgetPrefix = '' def __init__(self, name, default, doc='', title='', on_change=False, proxy=None, **kwd): self.name = name self.title = title if title else name self._default = default self._doc = doc self.on_change = on_change self.proxy = proxy def _declare_label(self): cog.out(f''' QLabel *label{self.name} = nullptr;''') def declare_label(self): if self.proxy: self.proxy.declare_label(self) else: self._declare_label() def _init_label(self, row, group_name): cog.out(f''' label{self.name} = new QLabel(this); layout{group_name}->addWidget(label{self.name}, {row}, 0);''') def init_label(self, row, group_name): if self.proxy: self.proxy.init_label(self, row, group_name) else: self._init_label(row, group_name) def _declare_widget(self): self.declare_label() cog.out(f''' {self.widget_type} *{self.widget_name} = nullptr;''') def declare_widget(self): if self.proxy: self.proxy.declare_widget(self) else: self._declare_widget() def _init_widget(self, row, group_name): self.init_label(row, group_name) cog.out(f''' {self.widget_name} = new {self.widget_type}(this); layout{group_name}->addWidget({self.widget_name}, {row}, {self.widget_column});''') if self.widget_setter: cog.out(f''' {self.widget_name}->{self.widget_setter}({self.namespace}::{self.class_name}::default{self.name}());''') self._init_pref_widget() def _init_pref_widget(self): cog.out(f''' {self.widget_name}->setEntryName("{self.name}");''') if self.path.startswith(_ParamPrefix): cog.out(f''' {self.widget_name}->setParamGrpPath("{self.path[len(_ParamPrefix):]}");''') else: cog.out(f''' {self.widget_name}->setParamGrpPath("{self.path}");''') def init_widget(self, row, group_name): if self.proxy: self.proxy.init_widget(self, row, group_name) else: self._init_widget(row, group_name) def _widget_save(self): cog.out(f''' {self.widget_name}->onSave();''') def widget_save(self): if self.proxy: self.proxy.widget_save(self) else: self._widget_save() def _widget_restore(self): cog.out(f''' {self.widget_name}->onRestore();''') def widget_restore(self): if self.proxy: self.proxy.widget_restore(self) else: self._widget_restore() def _retranslate_label(self): cog.out(f''' label{self.name}->setText(QObject::tr("{self.title}")); label{self.name}->setToolTip({self.widget_name}->toolTip());''') def retranslate_label(self): if self.proxy: self.proxy.retranslate_label(self) else: self._retranslate_label() def _retranslate(self): cog.out(f''' {self.widget_name}->setToolTip(QApplication::translate("{self.class_name}", {self.namespace}::{self.class_name}::doc{self.name}()));''') self.retranslate_label() def retranslate(self): if self.proxy: self.proxy.retranslate(self) else: self._retranslate() @property def default(self): return self._default def doc(self, class_name): if not self._doc: return '""' return f'''QT_TRANSLATE_NOOP("{class_name}", {quote(self._doc)})''' @property def widget_type(self): if self.proxy: return self.proxy.widget_type(self) return self.WidgetType @property def widget_prefix(self): if self.proxy: return self.proxy.widget_prefix(self) return self.WidgetPrefix @property def widget_setter(self): if self.proxy: return self.proxy.widget_setter(self) return self.WidgetSetter @property def widget_name(self): return f'{self.widget_prefix}{self.name}' @property def widget_column(self): return 1 def getter(self, handle): return f'{handle}->Get{self.Type}("{self.name}", {self.default})' def setter(self): return f'instance()->handle->Set{self.Type}("{self.name}",v)' class ParamBool(Param): Type = 'Bool' C_Type = 'bool' WidgetType = 'Gui::PrefCheckBox' WidgetSetter = 'setChecked' @property def default(self): if isinstance(self._default, str): return self._default return 'true' if self._default else 'false' def _declare_label(self): pass def _init_label(self, _row, _group_name): pass @property def widget_column(self): return 0 def _retranslate_label(self): cog.out(f''' {self.widget_name}->setText(QObject::tr("{self.title}"));''') class ParamFloat(Param): Type = 'Float' C_Type = 'double' WidgetType = 'Gui::PrefDoubleSpinBox' WidgetSetter = 'setValue' class ParamString(Param): Type = 'ASCII' C_Type = 'std::string' WidgetType = 'Gui::PrefLineEdit' WidgetSetter = 'setText' @property def default(self): return f'"{self._default}"' class ParamQString(Param): Type = 'ASCII' C_Type = 'QString' WidgetType = 'Gui::PrefLineEdit' WidgetSetter = 'setText' @property def default(self): return f'QStringLiteral("{self._default}")' def getter(self, handle): return f'QString::fromUtf8({handle}->Get{self.Type}("{self.name}", "{self._default}").c_str())' def setter(self): return f'instance()->handle->Set{self.Type}("{self.name}",v.toUtf8().constData())' class ParamInt(Param): Type = 'Int' C_Type = 'long' WidgetType = 'Gui::PrefSpinBox' WidgetSetter = 'setValue' class ParamUInt(Param): Type = 'Unsigned' C_Type = 'unsigned long' WidgetType = 'Gui::PrefSpinBox' WidgetSetter = 'setValue' class ParamHex(ParamUInt): @property def default(self): return '0x%08X' % self._default class ParamProxy: WidgetType = None WidgetPrefix = '' WidgetSetter = None def __init__(self, param_bool=None): self.param_bool = param_bool def declare_label(self, param): if not self.param_bool: param._declare_label() def widget_prefix(self, param): return self.WidgetPrefix if self.WidgetPrefix else param.WidgetPrefix def widget_type(self, param): return self.WidgetType if self.WidgetType else param.WidgetType def widget_setter(self, param): return self.WidgetSetter if self.WidgetSetter else param.WidgetSetter def declare_widget(self, param): if self.param_bool: self.param_bool.declare_widget() param._declare_widget() def init_label(self, param, row, group_name): if not self.param_bool: param._init_label(row, group_name) def init_widget(self, param, row, group_name): param._init_widget(row, group_name) if self.param_bool: self.param_bool.init_widget(row, group_name) cog.out(f''' {param.widget_name}->setEnabled({self.param_bool.widget_name}->isChecked()); connect({self.param_bool.widget_name}, SIGNAL(toggled(bool)), {param.widget_name}, SLOT(setEnabled(bool)));''') def retranslate_label(self, param): if not self.param_bool: param._retranslate_label() def retranslate(self, param): param._retranslate() if self.param_bool: self.param_bool.retranslate() def widget_save(self, param): param._widget_save() if self.param_bool: self.param_bool.widget_save() def widget_restore(self, param): param._widget_restore() if self.param_bool: self.param_bool.widget_restore() class ComboBoxItem: def __init__(self, text, tooltips=None, data=None): self.text = text self.tooltips = tooltips self._data = data @property def data(self): if self._data is None: return 'QVariant()' if isinstance(self._data, str): return f'QByteArray("{self._data}")' return self._data class ParamComboBox(ParamProxy): WidgetType = 'Gui::PrefComboBox' def __init__(self, items, translate=True, param_bool=None): super().__init__(param_bool) self.translate = translate self.items = [] for item in items: if isinstance(item, str): item = ComboBoxItem(item); elif isinstance(item, tuple): item = ComboBoxItem(*item) else: assert(isinstance(item, ComboBoxItem)) self.items.append(item) def widget_setter(self, _param): return None def init_widget(self, param, row, group_name): super().init_widget(param, row, group_name) if self.translate: cog.out(f''' for (int i=0; i<{len(self.items)}; ++i) {trace_comment()} {param.widget_name}->addItem(QString());''') for i,item in enumerate(self.items): if not self.translate: cog.out(f''' {param.widget_name}->addItem(QStringLiteral("{item.text}"));''') if item._data is not None: cog.out(f''' {param.widget_name}->setItemData({param.widget_name}->count()-1, {item.data});''') cog.out(f''' {param.widget_name}->setCurrentIndex({param.namespace}::{param.class_name}::default{param.name}());''') def retranslate(self, param): super().retranslate(param) cog.out(f''' {trace_comment()}''') for i,item in enumerate(self.items): if self.translate: cog.out(f''' {param.widget_name}->setItemText({i}, QObject::tr("{item.text}"));''') if item.tooltips: cog.out(f''' {param.widget_name}->setItemData({i}, QObject::tr("{item.tooltips}"), Qt::ToolTipRole);''') class ParamLinePattern(ParamProxy): WidgetType = 'Gui::PrefLinePattern' def widget_setter(self, _param): return None def init_widget(self, param, row, group_name): super().init_widget(param, row, group_name) cog.out(f''' {trace_comment()} for (int i=1; i<{param.widget_name}->count(); ++i) {{ if ({param.widget_name}->itemData(i).toInt() == {param.default}) {param.widget_name}->setCurrentIndex(i); }}''') class ParamColor(ParamProxy): WidgetType = 'Gui::PrefColorButton' WidgetSetter = 'setPackedColor' def __init__(self, param_bool=None, transparency=True): super().__init__(param_bool) self.transparency = transparency def init_widget(self, param, row, group_name): super().init_widget(param, row, group_name) if self.transparency: cog.out(f''' {param.widget_name}->setAllowTransparency(true);''') class ParamFile(ParamProxy): WidgetType = 'Gui::PrefFileChooser' WidgetSetter = 'setFileNameStd' class ParamSpinBox(ParamProxy): def __init__(self, value_min, value_max, value_step, decimals=0, param_bool=None): super().__init__(param_bool) self.value_min = value_min self.value_max = value_max self.value_step = value_step self.decimals = decimals def init_widget(self, param, row, group_name): super().init_widget(param, row, group_name) cog.out(f''' {trace_comment()} {param.widget_name}->setMinimum({self.value_min}); {param.widget_name}->setMaximum({self.value_max}); {param.widget_name}->setSingleStep({self.value_step});''') if self.decimals: cog.out(f''' {param.widget_name}->setDecimals({self.decimals});''') class ParamShortcutEdit(ParamProxy): WidgetType = 'Gui::PrefAccelLineEdit' WidgetSetter = 'setDisplayText' class Property: def __init__(self, name, property_type, doc, group=None, prop_flags=None, static=False): self.name = name self.type_name = property_type self.doc = doc self.prop_flags = prop_flags if prop_flags else 'App::Prop_None' self.static = static self.group = group if group else "" def declare(self): if self.static: cog.out(f''' static {self.type_name} *get{self.name}Property(App::DocumentObject *obj, bool force=false); inline {self.type_name} *get{self.name}Property(bool force=false) {{ return get{self.name}Property(this, force); }}''') else: cog.out(f''' {self.type_name} *get{self.name}Property(bool force=false);''') def define(self, class_name): if self.static: cog.out(f''' {trace_comment()} {self.type_name} *{class_name}::get{self.name}Property(App::DocumentObject *obj, bool force) {{''') else: cog.out(f''' {trace_comment()} {self.type_name} *{class_name}::get{self.name}Property(bool force) {{ auto obj = this;''') cog.out(f''' if (auto prop = Base::freecad_dynamic_cast<{self.type_name}>( obj->getPropertyByName("{self.name}"))) {{ if (prop->getContainer() == obj) return prop; }} if (!force) return nullptr; return static_cast<{self.type_name}*>(obj->addDynamicProperty( "{self.type_name}", "{self.name}", "{self.group}", {quote(self.doc)}, {self.prop_flags})); }} ''') def declare_properties(properties): cog.out(f''' {trace_comment()}''') for prop in properties: prop.declare() def define_properties(properties, class_name): for prop in properties: prop.define(class_name)