diff --git a/src/Tools/params_utils.py b/src/Tools/params_utils.py new file mode 100644 index 0000000000..98911a53c1 --- /dev/null +++ b/src/Tools/params_utils.py @@ -0,0 +1,1018 @@ +# -*- 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)