diff --git a/src/Gui/Action.cpp b/src/Gui/Action.cpp index 3ab26d391b..aac73eaf92 100644 --- a/src/Gui/Action.cpp +++ b/src/Gui/Action.cpp @@ -37,6 +37,7 @@ # include #endif +#include #include "Action.h" #include "Application.h" #include "Command.h" @@ -92,7 +93,7 @@ void Action::addTo(QWidget *w) */ void Action::onActivated () { - _pcCmd->invoke(0); + _pcCmd->invoke(0,Command::TriggerAction); } /** @@ -100,11 +101,13 @@ void Action::onActivated () */ void Action::onToggled(bool b) { - _pcCmd->invoke( b ? 1 : 0 ); + _pcCmd->invoke( b ? 1 : 0 , Command::TriggerAction); } void Action::setCheckable(bool b) { + if(b == _action->isCheckable()) + return; _action->setCheckable(b); if (b) { disconnect(_action, SIGNAL(triggered(bool)), this, SLOT(onActivated())); @@ -116,9 +119,14 @@ void Action::setCheckable(bool b) } } -void Action::setChecked(bool b) +void Action::setChecked(bool b, bool no_signal) { + bool blocked; + if(no_signal) + blocked = _action->blockSignals(true); _action->setChecked(b); + if(no_signal) + _action->blockSignals(blocked); } bool Action::isChecked() const @@ -154,6 +162,11 @@ void Action::setIcon (const QIcon & icon) _action->setIcon(icon); } +QIcon Action::icon () const +{ + return _action->icon(); +} + void Action::setStatusTip(const QString & s) { _action->setStatusTip(s); @@ -206,7 +219,7 @@ void Action::setMenuRole(QAction::MenuRole menuRole) * to the command object. */ ActionGroup::ActionGroup ( Command* pcCmd,QObject * parent) - : Action(pcCmd, parent), _group(0), _dropDown(false) + : Action(pcCmd, parent), _group(0), _dropDown(false),_external(false),_toggle(false) { _group = new QActionGroup(this); connect(_group, SIGNAL(triggered(QAction*)), this, SLOT(onActivated (QAction*))); @@ -319,16 +332,13 @@ void ActionGroup::setCheckedAction(int i) */ void ActionGroup::onActivated () { - _pcCmd->invoke(this->property("defaultAction").toInt()); + _pcCmd->invoke(this->property("defaultAction").toInt(), Command::TriggerAction); } -/** - * Activates the command. - */ -void ActionGroup::onActivated (int index) +void ActionGroup::onToggled(bool) { - _pcCmd->invoke(index); -} + onActivated(); +} /** * Activates the command. @@ -337,6 +347,9 @@ void ActionGroup::onActivated (QAction* a) { int index = _group->actions().indexOf(a); + // Calling QToolButton::setIcon() etc. has no effect if it has QAction set. + // We have to change the QAction icon instead +#if 0 QList widgets = a->associatedWidgets(); for (QList::iterator it = widgets.begin(); it != widgets.end(); ++it) { QMenu* menu = qobject_cast(*it); @@ -344,12 +357,17 @@ void ActionGroup::onActivated (QAction* a) QToolButton* button = qobject_cast(menu->parent()); if (button) { button->setIcon(a->icon()); + button->setText(a->text()); + button->setToolTip(a->toolTip()); this->setProperty("defaultAction", QVariant(index)); } } } - - _pcCmd->invoke(index); +#endif + this->setIcon(a->icon()); + this->setToolTip(a->toolTip()); + this->setProperty("defaultAction", QVariant(index)); + _pcCmd->invoke(index, Command::TriggerChildAction); } void ActionGroup::onHovered (QAction *a) diff --git a/src/Gui/Action.h b/src/Gui/Action.h index 893a9febab..bf72a7f44e 100644 --- a/src/Gui/Action.h +++ b/src/Gui/Action.h @@ -54,12 +54,13 @@ public: virtual void setVisible(bool); void setCheckable(bool); - void setChecked (bool); + void setChecked (bool, bool no_signal=false); bool isChecked() const; void setShortcut (const QString &); QKeySequence shortcut() const; void setIcon (const QIcon &); + QIcon icon() const; void setStatusTip (const QString &); QString statusTip() const; void setText (const QString &); @@ -69,6 +70,7 @@ public: void setWhatsThis (const QString &); QString whatsThis() const; void setMenuRole(QAction::MenuRole menuRole); + QAction *action() {return _action;}; public Q_SLOTS: virtual void onActivated (); @@ -112,13 +114,15 @@ public: public Q_SLOTS: void onActivated (); - void onActivated (int); + void onToggled(bool); void onActivated (QAction*); void onHovered (QAction*); protected: QActionGroup* _group; bool _dropDown; + bool _external; + bool _toggle; }; // -------------------------------------------------------------------- diff --git a/src/Gui/Command.cpp b/src/Gui/Command.cpp index 796d0eb365..720507b241 100644 --- a/src/Gui/Command.cpp +++ b/src/Gui/Command.cpp @@ -34,6 +34,11 @@ # include #endif +#include + +#include +#include + #include "Command.h" #include "Action.h" #include "Application.h" @@ -55,10 +60,13 @@ #include #include #include +#include #include #include +#include +FC_LOG_LEVEL_INIT("Command", true, true); using Base::Interpreter; using namespace Gui; @@ -218,6 +226,7 @@ Command::Command(const char* name) sGroup = QT_TR_NOOP("Standard"); eType = AlterDoc | Alter3DView | AlterSelection; bEnabled = true; + bCanLog = true; } Command::~Command() @@ -238,18 +247,26 @@ bool Command::isViewOfType(Base::Type t) const void Command::addTo(QWidget *pcWidget) { - if (!_pcAction) + if (!_pcAction) { _pcAction = createAction(); + testActive(); + } _pcAction->addTo(pcWidget); } void Command::addToGroup(ActionGroup* group, bool checkable) { - if (!_pcAction) - _pcAction = createAction(); - + addToGroup(group); _pcAction->setCheckable(checkable); +} + +void Command::addToGroup(ActionGroup* group) +{ + if (!_pcAction) { + _pcAction = createAction(); + testActive(); + } group->addAction(_pcAction->findChild()); } @@ -286,8 +303,84 @@ App::DocumentObject* Command::getObject(const char* Name) const return 0; } -void Command::invoke(int i) +int Command::_busy; + +class PendingLine { +public: + PendingLine(MacroManager::LineType type, const char *line) { + Application::Instance->macroManager()->addLine(type,line,true); + } + ~PendingLine() { + cancel(); + } + void cancel() { + Application::Instance->macroManager()->addLine(MacroManager::Cmt,0,true); + } +}; + +class CommandTrigger { +public: + CommandTrigger(Command::TriggerSource &trigger, Command::TriggerSource source) + :trigger(trigger),saved(trigger) + { + trigger = source; + } + + ~CommandTrigger() { + trigger = saved; + } +private: + Command::TriggerSource &trigger; + Command::TriggerSource saved; +}; + +void Command::setupCheckable(int iMsg) { + QAction *action = 0; + Gui::ActionGroup* pcActionGroup = qobject_cast(_pcAction); + if(pcActionGroup) { + QList a = pcActionGroup->actions(); + assert(iMsg < a.size()); + action = a[iMsg]; + }else + action = _pcAction->action(); + + if(!action) + return; + + bool checkable = action->isCheckable(); + _pcAction->setCheckable(checkable); + if(checkable) { + bool checked = false; + switch(triggerSource()) { + case TriggerNone: + checked = !action->isChecked(); + break; + case TriggerAction: + checked = _pcAction->isChecked(); + break; + case TriggerChildAction: + checked = action->isChecked(); + break; + } + bool blocked = action->blockSignals(true); + action->setChecked(checked); + action->blockSignals(blocked); + if(action!=_pcAction->action()) + _pcAction->setChecked(checked,true); + } + +} + +void Command::invoke(int i, TriggerSource trigger) { + CommandTrigger cmdTrigger(_trigger,trigger); + if(displayText.empty()) { + displayText = getMenuText(); + boost::replace_all(displayText,"&",""); + if(displayText.empty()) + displayText = getName(); + } + App::AutoTransaction committer((eType&NoTransaction)?0:displayText.c_str(),true); // Do not query _pcAction since it isn't created necessarily #ifdef FC_LOGUSERACTION Base::Console().Log("CmdG: %s\n",sName); @@ -295,9 +388,45 @@ void Command::invoke(int i) // set the application module type for the macro getGuiApplication()->macroManager()->setModule(sAppModule); try { + std::unique_ptr disabler; + if(bCanLog && !_busy) + disabler.reset(new LogDisabler); // check if it really works NOW (could be a delay between click deactivation of the button) - if (isActive()) - activated( i ); + if (isActive()) { + auto manager = getGuiApplication()->macroManager(); + if(!disabler) + activated( i ); + else { + Gui::SelectionLogDisabler disabler; + auto lines = manager->getLines(); + std::ostringstream ss; + ss << "### Begin command " << sName; + // Add a pending line to mark the start of a command + PendingLine pending(MacroManager::Cmt, ss.str().c_str()); + activated( i ); + ss.str(""); + if(manager->getLines() == lines) { + // This command does not record any lines, lets do it for + // him. The above LogDisabler is to prevent nested command + // logging, i.e. we only auto log the first invoking + // command. + + // Cancel the above pending line first + pending.cancel(); + ss << "Gui.runCommand('" << sName << "'," << i << ')'; + if(eType & AlterDoc) + manager->addLine(MacroManager::App, ss.str().c_str()); + else + manager->addLine(MacroManager::Gui, ss.str().c_str()); + }else{ + // In case the command has any output to the console, lets + // mark the end of the command here + ss << "### End command " << sName; + manager->addLine(MacroManager::Cmt, ss.str().c_str()); + } + } + getMainWindow()->updateActions(); + } } catch (const Base::SystemExitException&) { throw; @@ -350,6 +479,19 @@ void Command::testActive(void) } } + Gui::ActionGroup* pcAction = qobject_cast(_pcAction); + if(pcAction) { + Gui::CommandManager &rcCmdMgr = Gui::Application::Instance->commandManager(); + for(auto action : pcAction->actions()) { + auto name = action->property("CommandName").toByteArray(); + if(!name.size()) + continue; + Command* cmd = rcCmdMgr.getCommandByName(name); + if(cmd) + action->setEnabled(cmd->isActive()); + } + } + bool bActive = isActive(); _pcAction->setEnabled(bActive); } @@ -381,11 +523,35 @@ Gui::SelectionSingleton& Command::getSelection(void) return Gui::Selection(); } -std::string Command::getUniqueObjectName(const char *BaseName) const +std::string Command::getUniqueObjectName(const char *BaseName, const App::DocumentObject *obj) const { - assert(hasActiveDocument()); + auto doc = obj?obj->getDocument():App::GetApplication().getActiveDocument(); + assert(doc); + return doc->getUniqueObjectName(BaseName); +} - return getActiveGuiDocument()->getDocument()->getUniqueObjectName(BaseName); +std::string Command::getObjectCmd(const char *Name, const App::Document *doc, + const char *prefix, const char *postfix, bool gui) +{ + if(!doc) doc = App::GetApplication().getActiveDocument(); + if(!doc || !Name) + return std::string("None"); + std::ostringstream str; + if(prefix) + str << prefix; + str << (gui?"Gui":"App") << ".getDocument('" << doc->getName() + << "').getObject('" << Name << "')"; + if(postfix) + str << postfix; + return str.str(); +} + +std::string Command::getObjectCmd(const App::DocumentObject *obj, + const char *prefix, const char *postfix, bool gui) +{ + if(!obj || !obj->getNameInDocument()) + return std::string("None"); + return getObjectCmd(obj->getNameInDocument(), obj->getDocument(), prefix, postfix,gui); } void Command::setAppModuleName(const char* s) @@ -406,7 +572,6 @@ void Command::setGroupName(const char* s) #endif } - //-------------------------------------------------------------------------- // UNDO REDO transaction handling //-------------------------------------------------------------------------- @@ -420,31 +585,24 @@ void Command::setGroupName(const char* s) */ void Command::openCommand(const char* sCmdName) { - // Using OpenCommand with no active document ! - assert(Gui::Application::Instance->activeDocument()); - - if (sCmdName) - Gui::Application::Instance->activeDocument()->openCommand(sCmdName); - else - Gui::Application::Instance->activeDocument()->openCommand("Command"); + if (!sCmdName) + sCmdName = "Command"; + App::GetApplication().setActiveTransaction(sCmdName); } void Command::commitCommand(void) { - assert(Gui::Application::Instance->activeDocument()); - Gui::Application::Instance->activeDocument()->commitCommand(); + App::GetApplication().closeActiveTransaction(); } void Command::abortCommand(void) { - assert(Gui::Application::Instance->activeDocument()); - Gui::Application::Instance->activeDocument()->abortCommand(); + App::GetApplication().closeActiveTransaction(true); } bool Command::hasPendingCommand(void) { - assert(Gui::Application::Instance->activeDocument()); - return Gui::Application::Instance->activeDocument()->hasPendingCommand(); + return !!App::GetApplication().getActiveTransaction(); } bool Command::_blockCmd = false; @@ -455,7 +613,7 @@ void Command::blockCommand(bool block) } /// Run a App level Action -void Command::doCommand(DoCmd_Type eType, const char* sCmd, ...) +void Command::_doCommand(const char *file, int line, DoCmd_Type eType, const char* sCmd, ...) { va_list ap; va_start(ap, sCmd); @@ -470,37 +628,68 @@ void Command::doCommand(DoCmd_Type eType, const char* sCmd, ...) Base::Console().Log("CmdC: %s\n", format.constData()); #endif - if (eType == Gui) - Gui::Application::Instance->macroManager()->addLine(MacroManager::Gui, format.constData()); - else - Gui::Application::Instance->macroManager()->addLine(MacroManager::App, format.constData()); + _runCommand(file,line,eType,format.constData()); +} - Base::Interpreter().runString(format.constData()); +void Command::printPyCaller() { + if(!FC_LOG_INSTANCE.isEnabled(FC_LOGLEVEL_LOG)) + return; + PyFrameObject* frame = PyEval_GetFrame(); + if(!frame) + return; + int line = PyFrame_GetLineNumber(frame); +#if PY_MAJOR_VERSION >= 3 + const char *file = PyUnicode_AsUTF8(frame->f_code->co_filename); +#else + const char *file = PyString_AsString(frame->f_code->co_filename); +#endif + printCaller(file?file:"",line); +} + +void Command::printCaller(const char *file, int line) { + if(!FC_LOG_INSTANCE.isEnabled(FC_LOGLEVEL_LOG)) + return; + std::ostringstream str; +#ifdef FC_OS_WIN32 + const char *_f = std::strstr(file, "\\src\\"); +#else + const char *_f = std::strstr(file, "/src/"); +#endif + str << "# " << (_f?_f+5:file)<<'('<macroManager()->addLine(MacroManager::Cmt,str.str().c_str()); } /// Run a App level Action -void Command::runCommand(DoCmd_Type eType, const char* sCmd) +void Command::_runCommand(const char *file, int line, DoCmd_Type eType, const char* sCmd) { + LogDisabler d1; + SelectionLogDisabler d2; + Base::PyGILStateLocker lock; + + printCaller(file,line); if (eType == Gui) Gui::Application::Instance->macroManager()->addLine(MacroManager::Gui,sCmd); else Gui::Application::Instance->macroManager()->addLine(MacroManager::App,sCmd); - Base::Interpreter().runString(sCmd); + + try { + Base::Interpreter().runString(sCmd); + }catch(Py::Exception &) { + Base::PyException::ThrowException(); + } } /// Run a App level Action -void Command::runCommand(DoCmd_Type eType, const QByteArray& sCmd) +void Command::_runCommand(const char *file, int line, DoCmd_Type eType, const QByteArray& sCmd) { - if (eType == Gui) - Gui::Application::Instance->macroManager()->addLine(MacroManager::Gui,sCmd.constData()); - else - Gui::Application::Instance->macroManager()->addLine(MacroManager::App,sCmd.constData()); - Base::Interpreter().runString(sCmd.constData()); + _runCommand(file,line,eType,sCmd.constData()); } void Command::addModule(DoCmd_Type eType,const char* sModuleName) { if(alreadyLoadedModule.find(sModuleName) == alreadyLoadedModule.end()) { + LogDisabler d1; + SelectionLogDisabler d2; std::string sCmd("import "); sCmd += sModuleName; if (eType == Gui) @@ -512,7 +701,7 @@ void Command::addModule(DoCmd_Type eType,const char* sModuleName) } } -std::string Command::assureWorkbench(const char * sName) +std::string Command::_assureWorkbench(const char *file, int line, const char * sName) { // check if the WB is already open? std::string actName = WorkbenchManager::instance()->active()->name(); @@ -521,20 +710,67 @@ std::string Command::assureWorkbench(const char * sName) return actName; // else - switch to new WB - doCommand(Gui,"Gui.activateWorkbench('%s')",sName); + _doCommand(file,line,Gui,"Gui.activateWorkbench('%s')",sName); return actName; } -void Command::copyVisual(const char* to, const char* attr, const char* from) +void Command::_copyVisual(const char *file, int line, const char* to, const char* attr, const char* from) { - doCommand(Gui,"Gui.ActiveDocument.%s.%s=Gui.ActiveDocument.%s.%s", to, attr, from, attr); + _copyVisual(file,line,to,attr,from,attr); } -void Command::copyVisual(const char* to, const char* attr_to, const char* from, const char* attr_from) +void Command::_copyVisual(const char *file, int line, const char* to, const char* attr_to, const char* from, const char* attr_from) { - doCommand(Gui,"Gui.ActiveDocument.%s.%s=Gui.ActiveDocument.%s.%s", to, attr_to, from, attr_from); + auto doc = App::GetApplication().getActiveDocument(); + if(!doc) + return; + return _copyVisual(file,line,doc->getObject(to),attr_to, + doc->getObject(from),attr_from); +} + +void Command::_copyVisual(const char *file, int line, const App::DocumentObject *to, const char* attr_to, const App::DocumentObject *from, const char *attr_from) +{ + if(!from || !from->getNameInDocument() || !to || !to->getNameInDocument()) + return; + static std::map attrMap = { + {"ShapeColor","ShapeMaterial.DiffuseColor"}, + // {"LineColor","ShapeMaterial.DiffuseColor"}, + // {"PointColor","ShapeMaterial.DiffuseColor"}, + {"Transparency","Transparency"}, + }; + auto it = attrMap.find(attr_to); + auto objCmd = getObjectCmd(to); + if(it!=attrMap.end()) { + auto obj = from; + for(int depth=0;;++depth) { + auto vp = dynamic_cast( + Gui::Application::Instance->getViewProvider(obj)); + if(vp && vp->OverrideMaterial.getValue()) { + _doCommand(file,line,Gui,"%s.ViewObject.%s=%s.ViewObject.%s", + objCmd.c_str(),attr_to,getObjectCmd(obj).c_str(),it->second.c_str()); + return; + } + auto linked = obj->getLinkedObject(false,0,false,depth); + if(!linked || linked==obj) + break; + obj = linked; + } + } + + try { + _doCommand(file,line,Gui, + "%s.ViewObject.%s=getattr(%s.getLinkedObject(True).ViewObject,'%s',%s.ViewObject.%s)", + objCmd.c_str(),attr_to,getObjectCmd(from).c_str(),attr_from,objCmd.c_str(),attr_to); + }catch(Base::Exception &e) { + // e.ReportException(); + } +} + +void Command::_copyVisual(const char *file, int line, const App::DocumentObject *to, const char* attr, const App::DocumentObject *from) +{ + _copyVisual(file,line,to,attr,from,attr); } std::string Command::getPythonTuple(const std::string& name, const std::vector& subnames) @@ -899,6 +1135,8 @@ PythonCommand::PythonCommand(const char* name, PyObject * pcPyCommand, const cha type += int(AlterSelection); if (cmdType.find("ForEdit") != std::string::npos) type += int(ForEdit); + if (cmdType.find("NoTransaction") != std::string::npos) + type += int(NoTransaction); eType = type; } } @@ -1149,6 +1387,8 @@ void PythonGroupCommand::activated(int iMsg) assert(iMsg < a.size()); QAction* act = a[iMsg]; + setupCheckable(iMsg); + Base::PyGILStateLocker lock; Py::Object cmd(_pcPyCommand); if (cmd.hasAttr("Activated")) { @@ -1160,12 +1400,21 @@ void PythonGroupCommand::activated(int iMsg) // If the command group doesn't implement the 'Activated' method then invoke the command directly else { Gui::CommandManager &rcCmdMgr = Gui::Application::Instance->commandManager(); - rcCmdMgr.runCommandByName(act->property("CommandName").toByteArray()); + auto cmd = rcCmdMgr.getCommandByName(act->property("CommandName").toByteArray()); + if(cmd) { + bool checked = act->isCheckable() && act->isChecked(); + cmd->invoke(checked?1:0,TriggerAction); + } } + // It is better to let ActionGroup::onActivated() to handle icon and + // text change. The net effect is that the GUI won't change by user + // inovking command through runCommandByName() +#if 0 // Since the default icon is reset when enabing/disabling the command we have // to explicitly set the icon of the used command. pcAction->setIcon(a[iMsg]->icon()); +#endif } catch(Py::Exception&) { Base::PyGILStateLocker lock; @@ -1180,6 +1429,7 @@ bool PythonGroupCommand::isActive(void) try { Base::PyGILStateLocker lock; Py::Object cmd(_pcPyCommand); + if (cmd.hasAttr("IsActive")) { Py::Callable call(cmd.getAttr("IsActive")); Py::Tuple args; @@ -1214,16 +1464,19 @@ Action * PythonGroupCommand::createAction(void) Gui::CommandManager &rcCmdMgr = Gui::Application::Instance->commandManager(); Py::Callable call(cmd.getAttr("GetCommands")); - Py::Tuple args; - Py::Tuple ret(call.apply(args)); - for (Py::Tuple::iterator it = ret.begin(); it != ret.end(); ++it) { + Py::Sequence args; + Py::Sequence ret(call.apply(args)); + for (auto it = ret.begin(); it != ret.end(); ++it) { Py::String str(*it); QAction* cmd = pcAction->addAction(QString()); cmd->setProperty("CommandName", QByteArray(static_cast(str).c_str())); PythonCommand* pycmd = dynamic_cast(rcCmdMgr.getCommandByName(cmd->property("CommandName").toByteArray())); - if (pycmd) { - cmd->setCheckable(pycmd->isCheckable()); + if (pycmd && pycmd->isCheckable()) { + cmd->setCheckable(true); + cmd->blockSignals(true); + cmd->setChecked(pycmd->isChecked()); + cmd->blockSignals(false); } } @@ -1233,15 +1486,18 @@ Action * PythonGroupCommand::createAction(void) defaultId = static_cast(def); } - // if the command is 'exclusive' then activate the default action - if (pcAction->isExclusive()) { - QList a = pcAction->actions(); - if (defaultId >= 0 && defaultId < a.size()) { - QAction* qtAction = a[defaultId]; - if (qtAction->isCheckable()) { + QList a = pcAction->actions(); + if (defaultId >= 0 && defaultId < a.size()) { + QAction* qtAction = a[defaultId]; + if (qtAction->isCheckable()) { + // if the command is 'exclusive' then activate the default action + if (pcAction->isExclusive()) { qtAction->blockSignals(true); qtAction->setChecked(true); qtAction->blockSignals(false); + }else if(qtAction->isCheckable()){ + pcAction->setCheckable(true); + pcAction->setChecked(qtAction->isChecked(),true); } } } diff --git a/src/Gui/Command.h b/src/Gui/Command.h index 6139c8cd46..698ee981c2 100644 --- a/src/Gui/Command.h +++ b/src/Gui/Command.h @@ -32,6 +32,167 @@ #include +/** @defgroup CommandMacros Helper macros for running commands through Python interpreter */ +//@{ + +/** Runs a command for accessing document attribute or method + * @param _type: type of document, Gui or App + * @param _doc: pointer to a document + * @param _cmd: command string, streamable + * + * Example: + * @code{.cpp} + * _FCMD_DOC_CMD(Gui,doc,"getObject('" << objName << "')"); + * @endcode + * + * Translates to command (assuming doc's name is 'DocName', and + * and objName constains value 'ObjName'): + * @code{.py} + * Gui.getDocument('DocName').getObject('ObjName') + * @endcode + */ +#define _FCMD_DOC_CMD(_type,_doc,_cmd) do{\ + auto __doc = _doc;\ + if(__doc && __doc->getName()) {\ + std::ostringstream _str;\ + _str << #_type ".getDocument('" << __doc->getName() << "')." << _cmd;\ + Gui::Command::runCommand(Gui::Command::Doc,_str.str().c_str());\ + }\ +}while(0) + +/** Runs a command for accessing App.Document attribute or method + * + * @param _doc: pointer to a document + * @param _cmd: command string, streamable + * @sa _FCMD_DOC_CMD() + */ +#define FCMD_DOC_CMD(_doc,_cmd) _FCMD_DOC_CMD(App,_doc,_cmd) + +/** Runs a command for accessing an object's document attribute or method + * @param _type: type of the document, Gui or App + * @param _obj: pointer to a DocumentObject + * @param _cmd: command string, streamable + */ +#define _FCMD_OBJ_DOC_CMD(_type,_obj,_cmd) do{\ + auto __obj = _obj;\ + if(__obj)\ + _FCMD_DOC_CMD(_type,__obj->getDocument(),_cmd);\ +}while(0) + +/** Runs a command for accessing an object's App::Document attribute or method + * @param _obj: pointer to a DocumentObject + * @param _cmd: command string, streamable + */ +#define FCMD_OBJ_DOC_CMD(_obj,_cmd) _FCMD_OBJ_DOC_CMD(App,_obj,_cmd) + +/** Runs a command for accessing an object's Gui::Document attribute or method + * @param _obj: pointer to a DocumentObject + * @param _cmd: command string, streamable + */ +#define FCMD_VOBJ_DOC_CMD(_obj,_cmd) _FCMD_OBJ_DOC_CMD(Gui,_obj,_cmd) + +/** Runs a command for accessing a document/view object's attribute or method + * @param _type: type of the object, Gui or App + * @param _obj: pointer to a DocumentObject + * @param _cmd: command string, streamable + * + * Example: + * @code{.cpp} + * _FCMD_OBJ_CMD(Gui,obj,"Visibility = " << (visible?"True":"False")); + * @endcode + * + * Translates to command (assuming obj's document name is 'DocName', obj's name + * is 'ObjName', and visible is true): + * @code{.py} + * Gui.getDocument('DocName').getObject('ObjName').Visibility = True + * @endcode + */ +#define _FCMD_OBJ_CMD(_type,_cmd_type,_obj,_cmd) do{\ + auto __obj = _obj;\ + if(__obj && __obj->getNameInDocument()) {\ + std::ostringstream _str;\ + _str << #_type ".getDocument('" << __obj->getDocument()->getName() \ + << "').getObject('" << __obj->getNameInDocument() << "')." << _cmd;\ + Gui::Command::runCommand(Gui::Command::_cmd_type,_str.str().c_str());\ + }\ +}while(0) + +/** Runs a command for accessing an document object's attribute or method + * @param _obj: pointer to a DocumentObject + * @param _cmd: command string, streamable + * @sa _FCMD_OBJ_CMD() + */ +#define FCMD_OBJ_CMD(_obj,_cmd) _FCMD_OBJ_CMD(App,Doc,_obj,_cmd) + +/** Runs a command for accessing an view object's attribute or method + * @param _obj: pointer to a DocumentObject + * @param _cmd: command string, streamable + * @sa _FCMD_OBJ_CMD() + */ +#define FCMD_VOBJ_CMD(_obj,_cmd) _FCMD_OBJ_CMD(Gui,Gui,_obj,_cmd) + +/** Runs a command for accessing a document object's attribute or method + * @param _cmd: command string, supporting printf like formatter + * @param _obj: pointer to a DocumentObject + * + * Example: + * @code{.cpp} + * FCMD_OBJ_CMD2("Visibility = %s", obj, visible?"True":"False"); + * @endcode + * + * Translates to command (assuming obj's document name is 'DocName', obj's name + * is 'ObjName', and visible is true): + * @code{.py} + * App.getDocument('DocName').getObject('ObjName').Visibility = True + * @endcode + */ +#define FCMD_OBJ_CMD2(_cmd,_obj,...) do{\ + auto __obj = _obj;\ + if(__obj && __obj->getNameInDocument()) {\ + Gui::Command::doCommand(Gui::Command::Doc,"App.getDocument('%s').getObject('%s')." _cmd,\ + __obj->getDocument()->getName(),__obj->getNameInDocument(),## __VA_ARGS__);\ + }\ +}while(0) + +/** Runs a command for accessing a view object's attribute or method + * @param _cmd: command string, supporting printf like formatter + * @param _obj: pointer to a DocumentObject + * @sa FCMD_OBJ_CMD2() + */ +#define FCMD_VOBJ_CMD2(_cmd,_obj,...) do{\ + auto __obj = _obj;\ + if(__obj && __obj->getNameInDocument()) {\ + Gui::Command::doCommand(Gui::Command::Gui,"Gui.getDocument('%s').getObject('%s')." _cmd,\ + __obj->getDocument()->getName(),__obj->getNameInDocument(),## __VA_ARGS__);\ + }\ +}while(0) + +/** Runs a command to start editing a give object + * @param _obj: pointer to a DocumentObject + * + * Unlike other FCMD macros, this macro editing the object using the current + * active document, instead of the object's owner document. This allows + * in-place editing an object, which may be brought in through linking to an + * external group. + */ +#define FCMD_SET_EDIT(_obj) do{\ + auto __obj = _obj;\ + if(__obj && __obj->getNameInDocument()) {\ + Gui::Command::doCommand(Gui::Command::Gui,\ + "Gui.ActiveDocument.setEdit(App.getDocument('%s').getObject('%s'))",\ + __obj->getDocument()->getName(), __obj->getNameInDocument());\ + }\ +}while(0) + + +/// Hides an object +#define FCMD_OBJ_HIDE(_obj) FCMD_OBJ_CMD(_obj,"Visibility = False") + +/// Shows an object +#define FCMD_OBJ_SHOW(_obj) FCMD_OBJ_CMD(_obj,"Visibility = True") + +//@} + class QWidget; class QByteArray; @@ -63,6 +224,7 @@ void CreateViewStdCommands(void); void CreateWindowStdCommands(void); void CreateStructureCommands(void); void CreateTestCommands(void); +void CreateLinkCommands(void); /** The CommandBase class @@ -135,6 +297,7 @@ protected: //@} protected: Action *_pcAction; /**< The Action item. */ + std::string displayText; }; /** The Command class. @@ -162,8 +325,6 @@ protected: //@{ /// Methods which gets called when activated, needs to be reimplemented! virtual void activated(int iMsg)=0; - /// Override this method if your Cmd is not always active - virtual bool isActive(void){return true;} /// Creates the used Action virtual Action * createAction(void); /// Applies the menu text, tool and status tip to the passed action object @@ -176,15 +337,35 @@ public: //@{ /// CommandManager is a friend friend class CommandManager; + /// Override this method if your Cmd is not always active + virtual bool isActive(void){return true;} /// Get somtile called to check the state of the command void testActive(void); /// Enables or disables the command void setEnabled(bool); - /// get called by the QAction - void invoke (int); + /// Command trigger source + enum TriggerSource { + /// No external trigger, e.g. invoked through Python + TriggerNone, + /// Command triggered by an action + TriggerAction, + /// Command triggered by a child action inside an action group + TriggerChildAction, + }; + /// Return the current command trigger source + TriggerSource triggerSource() const {return _trigger;} + /** Called to invoke the command + * + * @param index: in case of group command, this is the index of the child + * command. For checkable command, this indicates the + * checkable state. + * @param trigger: indicate the command triggering source, see TriggerSource. + */ + void invoke (int index, TriggerSource trigger=TriggerNone); /// adds this command to arbitrary widgets void addTo(QWidget *); void addToGroup(ActionGroup *, bool checkable); + void addToGroup(ActionGroup *); //@} @@ -206,8 +387,19 @@ public: bool isViewOfType(Base::Type t) const; /// returns the named feature or the active one from the active document or NULL App::DocumentObject* getObject(const char* Name) const; - /// Get unique Feature name from the active document - std::string getUniqueObjectName(const char *BaseName) const; + /// returns a python command string to retrieve an object from a document + static std::string getObjectCmd(const char *Name, const App::Document *doc=0, + const char *prefix=0, const char *postfix=0, bool gui=false); + /// returns a python command string to retrieve the given object + static std::string getObjectCmd(const App::DocumentObject *obj, + const char *prefix=0, const char *postfix=0, bool gui=false); + /** Get unique Feature name from the active document + * + * @param BaseName: the base name + * @param obj: if not zero, then request the unique name in the document of + * the given object. + */ + std::string getUniqueObjectName(const char *BaseName, const App::DocumentObject *obj=0) const; //@} /** @name Helper methods for the Undo/Redo and Update handling */ @@ -230,6 +422,8 @@ public: void languageChange(); /// Updates the QAction with respect to the passed mode. void updateAction(int mode); + /// Setup checkable actions based on current TriggerSource + void setupCheckable(int iMsg); //@} /** @name Helper methods for issuing commands to the Python interpreter */ @@ -245,17 +439,89 @@ public: }; /// Blocks all command objects static void blockCommand(bool); - /// Run a App level Action - static void doCommand(DoCmd_Type eType,const char* sCmd,...); - static void runCommand(DoCmd_Type eType,const char* sCmd); - static void runCommand(DoCmd_Type eType,const QByteArray& sCmd); + /// Print to Python console the current Python calling source file and line number + static void printPyCaller(); + /// Print to Python console the current calling source file and line number + static void printCaller(const char *file, int line); + + /** Convenience macro to run a command with printf like formatter + * + * @sa Command::_doCommand() + */ +#define doCommand(_type,_cmd,...) _doCommand(__FILE__,__LINE__,_type,_cmd,##__VA_ARGS__) + + /** Run a command with printf like formatter + * + * @param file: the calling file path (for debugging purpose) + * @param line: the calling line number (for debugging purpose) + * @param eType: command type, See DoCmd_Type + * @param sCmd: command string that supports printf like formatter + * + * You can use the convenience macro doCommand() to automate \c file and \c + * line arguments. You may also want to use various helper @ref CommandMacros. + */ + static void _doCommand(const char *file, int line, DoCmd_Type eType,const char* sCmd,...); + + /** Convenience macro to run a command + * + * @sa Command::_runCommand() + */ +#define runCommand(_type,_cmd) _runCommand(__FILE__,__LINE__,_type,_cmd) + + /** Run a command + * + * @param file: the calling file path (for debugging purpose) + * @param line: the calling line number (for debugging purpose) + * @param eType: command type, See DoCmd_Type + * @param sCmd: command string + * + * @sa _doCommand() + */ + static void _runCommand(const char *file, int line, DoCmd_Type eType,const char* sCmd); + + /** Run a command + * + * @param file: the calling file path (for debugging purpose) + * @param line: the calling line number (for debugging purpose) + * @param eType: command type, See DoCmd_Type + * @param sCmd: command string + * + * @sa _doCommand() + */ + static void _runCommand(const char *file, int line, DoCmd_Type eType,const QByteArray& sCmd); + /// import an external (or own) module only once static void addModule(DoCmd_Type eType,const char* sModuleName); - /// assures the switch to a certain workbench, if already in the workbench, does nothing. - static std::string assureWorkbench(const char * sName); - static void copyVisual(const char* to, const char* attr, const char* from); - static void copyVisual(const char* to, const char* attr_to, const char* from, const char* attr_from); + /** Convenience macro to assure the switch to a certain workbench + * + * @sa _assureWorkbench() + */ +#define assureWorkbench(_name) _assureWorkbench(__FILE__,__LINE__,_name) + + /** Assures the switch to a certain workbench + * + * @param file: the calling file path (for debugging purpose) + * @param line: the calling line number (for debugging purpose) + * @param sName: workbench name + * + * @return Return the current active workbench name before switching. + * + * If already in the workbench, does nothing. + */ + static std::string _assureWorkbench(const char *file, int line, const char * sName); + //@} + + /** @name Methods for copying visiual properties */ + //@{ + /// Convenience macro to copy visual properties +#define copyVisual(...) _copyVisual(__FILE__,__LINE__,## __VA_ARGS__) + static void _copyVisual(const char *file, int line, const char* to, const char* attr, const char* from); + static void _copyVisual(const char *file, int line, const char* to, const char* attr_to, const char* from, const char* attr_from); + static void _copyVisual(const char *file, int line, const App::DocumentObject *to, const char *attr, const App::DocumentObject *from); + static void _copyVisual(const char *file, int line, const App::DocumentObject *to, const char *attr_to, const App::DocumentObject *from, const char *attr_from); + //@} + /// Get Python tuple from object and sub-elements static std::string getPythonTuple(const std::string& name, const std::vector& subnames); /// translate a string to a python string literal (needed e.g. in file names for windows...) @@ -263,7 +529,6 @@ public: const std::string strToPython(const std::string &Str){ return strToPython(Str.c_str()); } - //@} /** @name Helper methods to generate help pages */ //@{ @@ -301,12 +566,25 @@ public: void adjustCameraPosition(); //@} + /// Helper class to disable python console log + class LogDisabler { + public: + LogDisabler() { + ++Command::_busy; + } + ~LogDisabler() { + --Command::_busy; + } + }; + friend class LogDisabler; + protected: enum CmdType { AlterDoc = 1, /**< Command change the Document */ Alter3DView = 2, /**< Command change the Gui */ AlterSelection = 4, /**< Command change the Selection */ - ForEdit = 8 /**< Command is in a special edit mode active */ + ForEdit = 8, /**< Command is in a special edit mode active */ + NoTransaction = 16, /**< Do not setup auto transaction */ }; /** @name Attributes @@ -320,10 +598,15 @@ protected: const char* sName; const char* sHelpUrl; int eType; + /// Indicate if the command shall log to MacroManager + bool bCanLog; //@} private: + static int _busy; bool bEnabled; static bool _blockCmd; + /// For storing the current command trigger source + TriggerSource _trigger = TriggerNone; }; /** The Python command class diff --git a/src/Gui/CommandStd.cpp b/src/Gui/CommandStd.cpp index 50d5fe7b17..1d8ee358c6 100644 --- a/src/Gui/CommandStd.cpp +++ b/src/Gui/CommandStd.cpp @@ -146,7 +146,7 @@ StdCmdRecentFiles::StdCmdRecentFiles() sToolTipText = QT_TR_NOOP("Recent file list"); sWhatsThis = "Std_RecentFiles"; sStatusTip = QT_TR_NOOP("Recent file list"); - eType = 0; + eType = NoTransaction; } /** diff --git a/src/Gui/CommandWindow.cpp b/src/Gui/CommandWindow.cpp index 6ecb19fc14..794f2167c9 100644 --- a/src/Gui/CommandWindow.cpp +++ b/src/Gui/CommandWindow.cpp @@ -142,7 +142,7 @@ StdCmdCloseActiveWindow::StdCmdCloseActiveWindow() // collide with this shortcut. Thus the shortcut of QMdiSubWindow will be // reset in MainWindow::addWindow() (#0002631) sAccel = keySequenceToAccel(QKeySequence::Close); - eType = 0; + eType = NoTransaction; } void StdCmdCloseActiveWindow::activated(int iMsg) diff --git a/src/Gui/Macro.cpp b/src/Gui/Macro.cpp index dd4999024e..dfe49d3ae8 100644 --- a/src/Gui/Macro.cpp +++ b/src/Gui/Macro.cpp @@ -54,7 +54,8 @@ MacroManager::MacroManager() scriptToPyConsole(true), localEnv(true), pyConsole(0), - pyDebugger(new PythonDebugger()) + pyDebugger(new PythonDebugger()), + totalLines(0) { // Attach to the Parametergroup regarding macros this->params = App::GetApplication().GetParameterGroupByPath("User parameter:BaseApp/Preferences/Macro"); @@ -166,8 +167,25 @@ void MacroManager::cancel(void) this->openMacro = false; } -void MacroManager::addLine(LineType Type, const char* sLine) +void MacroManager::addLine(LineType Type, const char* sLine, bool pending) { + if(pending) { + if(!sLine) + pendingLine.clear(); + else { + pendingType = Type; + pendingLine = sLine; + } + return; + } + if(!sLine) + return; + if(pendingLine.size() && pendingLine.c_str()!=sLine) { + addLine(pendingType,pendingLine.c_str()); + pendingLine.clear(); + } + + ++totalLines; if (this->openMacro) { bool comment = false; if (Type == Gui) { diff --git a/src/Gui/Macro.h b/src/Gui/Macro.h index 700f4c7b19..263cc3ac94 100644 --- a/src/Gui/Macro.h +++ b/src/Gui/Macro.h @@ -60,7 +60,7 @@ public: enum LineType { App, /**< The line effects only the document and Application (FreeCAD) */ Gui, /**< The line effects the Gui (FreeCADGui) */ - Cmt /**< The line is handled as a comment */ + Cmt, /**< The line is handled as a comment */ }; /** Opens a new Macro recording session @@ -80,7 +80,7 @@ public: /// indicates if a macro recording in in progress bool isOpen(void) const {return openMacro;} /// insert a new line in the macro - void addLine(LineType Type,const char* sLine); + void addLine(LineType Type,const char* sLine,bool pending=false); /** Set the active module * This is normally done by the workbench switch. It sets * the actually active application module so when the macro @@ -93,6 +93,9 @@ public: /** Observes its parameter group. */ void OnChange(Base::Subject &rCaller, const char * sReason); + /// Return the added lines regardless of recording or not + long getLines() const {return totalLines;} + protected: QStringList macroInProgress; /**< Container for the macro */ QString macroName; /**< name of the macro */ @@ -104,6 +107,9 @@ protected: PythonConsole* pyConsole; // link to the python console PythonDebugger* pyDebugger; Base::Reference params; // link to the Macro parameter group + long totalLines; + std::string pendingLine; + LineType pendingType; friend struct ApplicationP; };