View Issue Details

IDProjectCategoryView StatusLast Update
0015579ScribusScripterpublic2019-03-07 11:21
Reporterale Assigned To 
PrioritynormalSeverityfeatureReproducibilityN/A
Status newResolutionopen 
Product Version1.5.5.svn 
Target VersionFixed in Version 
Summary0015579: [PATCH] add user's scripts to the navigation
Descriptioncurrently, the scribus official scripts are added to "Scripter > Scribus Scripts".

i propose adding a "Scripter > Scripts" menu entry, if the user has own scripts.

where the own scripts are placed is defined in a new field in "file > preferences > Scripter".

- the text field can contain multiple paths separated by a comma.
- if no path is defined, no "Scripter > Scripts" menu entry is displayed.
- for a first implementation, the menu entries are the file name, without the .py extension.

please give feedback. i can try to implement this, if there is some sort of agreement hat his is interesting / useful.
Tagspatch
PatchYes

Activities

aiena

2019-02-21 14:09

reporter   ~0045908

I agree with this proposal.

I feel you should make the scripter preferences window tabbed like the "PDF Export" in preferences. Then keep the "Extensions" stuff as one tab and make a new tab called "Scripts" and there have a big QListWidget with add/delete buttons to add/delete paths to user scripts (optionally you could have a tickbox next to the path to enable/disable reading scripts from that path without deleting the path itself. May be handy for people who want to keep stable and dev version of same scripts in 2 folders and toggle for testing).

The plugin can store the paths in appdata internally with a "," or ";" whatever you prefer with escaped/quoted paths(optional) so a comma/semicolon in the path itself doesn't matter (I think linux allows ;/, in a path name ntfs does too the OS restricts it).

ale

2019-02-21 14:23

manager   ~0045909

adding a tab with a list is more work, than adding a text field...
i'd prefer to first program it as a simple text field, with semicolon separated values for now, and -- if we notice that people start using the feature and need a more friendly UI -- we can easily add it later.

so, i would do it the other way round: already store the single paths in the xml (so that we can easily move to a better solution), but show / edit it as a single text field.

aiena

2019-02-24 06:34

reporter   ~0045917

Yes initially a proof of concept is best. Impt question is should we allow the same script names on multiple folders. Currently the Action search shows the full path. But it may be nice to restrict same name scripts e.g. only the dev folder path or the stable folder path of the scripts can be used at a time. This validation could optionally be turned off but at a later point. I would be willing to test your logic even if you made a C++ variable where the paths are stored at compile time.

ale

2019-02-26 08:29

manager   ~0045924

imo, if two script have the same name, they should be shown with the same label. up to you to pick different names.

most users will have only one source anyway.

ale

2019-02-26 08:30

manager   ~0045925

note to myself:

- list all scripts at the root of the path
- list all scripts in sub directories that have the same name as the directory containing them.

ale

2019-02-28 12:34

manager   ~0045938

next feature: the listed directory can have a "project.xml" (in the future: yaml...) file with a list of scripts:

- we have to find a name for the "project.xml"
- it will contain the script name (shown in the navigation), the path to the script and optionally a defnition of where in the navigation the script should be shown (is it really needed?)
- if a "project" file is found, only the files listed in there are shown in the menus, otherwise all the files that match the requirements are listed. (*.py in main directory; main.py or directoryname.py in the sub directories)

ale

2019-02-28 12:49

manager   ~0045939

user scripts are only scanned when scribus starts.

a "rescan" command could be added at the end of the user scripts or in the preferences...

ale

2019-02-28 17:33

manager   ~0045940

here is a first patch

userscripts.patch (12,207 bytes)
From 73f42d8972746905014337babe9c30aa48babab1 Mon Sep 17 00:00:00 2001
From: ale rimoldi <ale@graphicslab.org>
Date: Thu, 28 Feb 2019 17:42:10 +0100
Subject: [PATCH] add a scripter > scripts with user's scripts

---
 .../plugins/scriptplugin/prefs_scripter.cpp   | 21 ++++
 .../scriptplugin/prefs_scripterbase.ui        | 14 +++
 scribus/plugins/scriptplugin/scriptercore.cpp | 99 +++++++++++++++++++
 scribus/plugins/scriptplugin/scriptercore.h   |  9 ++
 scribus/scraction.cpp                         |  4 +
 scribus/scraction.h                           |  2 +-
 6 files changed, 148 insertions(+), 1 deletion(-)

diff --git a/scribus/plugins/scriptplugin/prefs_scripter.cpp b/scribus/plugins/scriptplugin/prefs_scripter.cpp
index b4bcf8094a..856aa8d6b8 100644
--- a/scribus/plugins/scriptplugin/prefs_scripter.cpp
+++ b/scribus/plugins/scriptplugin/prefs_scripter.cpp
@@ -31,6 +31,17 @@ Prefs_Scripter::Prefs_Scripter(QWidget* parent)
 	// The startup script box should be disabled  if ext scripts are off
 	startupScriptEdit->setEnabled(extensionScriptsChk->isChecked());
 	startupScriptEdit->setText(scripterCore->startupScript());
+
+	PrefsContext* prefs = PrefsManager::instance()->prefsFile->getPluginContext("scriptplugin");
+	PrefsTable* prefUserScriptsPath = prefs->getTable("userscriptspaths");
+	QStringList paths{};
+	for (int i = 0; i < prefUserScriptsPath->getRowCount(); i++)
+	{
+		paths.append(prefUserScriptsPath->get(i, 0));
+	}
+	userScriptPathEdit->setText(paths.join(";"));
+	userScriptPathEdit->setToolTip( "<qt>" + tr("Multiple paths are separated by semicolons. You need to restart Scribus to see new scripts.") + "</qt>");
+
 	// signals and slots connections
 	connect(extensionScriptsChk, SIGNAL(toggled(bool)), startupScriptEdit, SLOT(setEnabled(bool)));
 	// colors
@@ -78,6 +89,16 @@ void Prefs_Scripter::apply()
 		prefs->set("syntaxstring", stringColor.name());
 		prefs->set("syntaxtext", textColor.name());
 
+		// qDebug() << userScriptPathEdit->text();
+		PrefsTable* prefUserScriptsPath = prefs->getTable("userscriptspaths");
+		prefUserScriptsPath->clear();
+		int i = 0;
+		for (auto path: userScriptPathEdit->text().split(";"))
+		{
+			prefUserScriptsPath->set(i, 0, path);
+			i++;
+		}
+
 		emit prefsChanged();
 	}
 }
diff --git a/scribus/plugins/scriptplugin/prefs_scripterbase.ui b/scribus/plugins/scriptplugin/prefs_scripterbase.ui
index 7c35b7505c..892a31b1e3 100644
--- a/scribus/plugins/scriptplugin/prefs_scripterbase.ui
+++ b/scribus/plugins/scriptplugin/prefs_scripterbase.ui
@@ -98,6 +98,20 @@
          </item>
         </layout>
        </item>
+       <item>
+        <layout class="QHBoxLayout" name="horizontalLayout_4">
+         <item>
+          <widget class="QLabel" name="label_3">
+           <property name="text">
+            <string>User Script:</string>
+           </property>
+          </widget>
+         </item>
+         <item>
+          <widget class="QLineEdit" name="userScriptPathEdit"/>
+         </item>
+        </layout>
+       </item>
        <item>
         <spacer name="verticalSpacer_2">
          <property name="orientation">
diff --git a/scribus/plugins/scriptplugin/scriptercore.cpp b/scribus/plugins/scriptplugin/scriptercore.cpp
index c9c8672cca..07548adb7a 100644
--- a/scribus/plugins/scriptplugin/scriptercore.cpp
+++ b/scribus/plugins/scriptplugin/scriptercore.cpp
@@ -15,6 +15,8 @@ for which a new license (GPL+exception) is in place.
 #include <QTextCodec>
 #include <QByteArray>
 #include <QPixmap>
+#include <QDir>
+#include <QDirIterator>
 #include <cstdlib>
 
 #include "runscriptdialog.h"
@@ -48,6 +50,7 @@ ScripterCore::ScripterCore(QWidget* parent)
 	pcon = new PythonConsole(parent);
 	scrScripterActions.clear();
 	scrRecentScriptActions.clear();
+	scrUserScriptActions.clear();
 	returnString = "init";
 
 	scrScripterActions.insert("scripterExecuteScript", new ScrAction(QObject::tr("&Execute Script..."), QKeySequence(), this));
@@ -90,6 +93,11 @@ void ScripterCore::addToMainWindowMenu(ScribusMainWindow *mw)
 	menuMgr->addMenuItemString("scripterExecuteScript", "Scripter");
 	menuMgr->createMenu("RecentScripts", QObject::tr("&Recent Scripts"), "Scripter", false, true);
 	menuMgr->addMenuItemString("RecentScripts", "Scripter");
+	if (m_userScriptsEnabled)
+	{
+		menuMgr->createMenu("UserScripts", QObject::tr("S&cripts"), "Scripter", false, true);
+		menuMgr->addMenuItemString("UserScripts", "Scripter");
+	}
 	menuMgr->addMenuItemString("scripterExecuteScript", "Scripter");
 	menuMgr->addMenuItemString("SEPARATOR", "Scripter");
 	menuMgr->addMenuItemString("scripterShowConsole", "Scripter");
@@ -101,6 +109,10 @@ void ScripterCore::addToMainWindowMenu(ScribusMainWindow *mw)
 	menuMgr->addMenuItemStringstoMenuBar("Scripter", scrScripterActions);
 	RecentScripts = SavedRecentScripts;
 	rebuildRecentScriptsMenu();
+	if (m_userScriptsEnabled)
+	{
+		rebuildUserScriptsMenu();
+	}
 }
 
 void ScripterCore::enableMainWindowMenu()
@@ -109,6 +121,10 @@ void ScripterCore::enableMainWindowMenu()
 		return;
 	menuMgr->setMenuEnabled("ScribusScripts", true);
 	menuMgr->setMenuEnabled("RecentScripts", true);
+	if (m_userScriptsEnabled)
+	{
+		menuMgr->setMenuEnabled("UserScripts", true);
+	}
 	scrScripterActions["scripterExecuteScript"]->setEnabled(true);
 }
 
@@ -118,6 +134,10 @@ void ScripterCore::disableMainWindowMenu()
 		return;
 	menuMgr->setMenuEnabled("ScribusScripts", false);
 	menuMgr->setMenuEnabled("RecentScripts", false);
+	if (m_userScriptsEnabled)
+	{
+		menuMgr->setMenuEnabled("UserScripts", false);
+	}
 	scrScripterActions["scripterExecuteScript"]->setEnabled(false);
 }
 
@@ -156,6 +176,20 @@ void ScripterCore::rebuildRecentScriptsMenu()
 	menuMgr->addMenuItemStringstoRememberedMenu("RecentScripts", scrRecentScriptActions);
 }
 
+void ScripterCore::rebuildUserScriptsMenu()
+{
+	menuMgr->clearMenuStrings("UserScripts");
+	scrUserScriptActions.clear();
+	for (auto script: m_userScripts)
+	{
+		QString menuEntry = QFileInfo(script).baseName();
+		scrUserScriptActions.insert(menuEntry, new ScrAction( ScrAction::RecentScript, menuEntry, QKeySequence(), this, script));
+		connect( scrUserScriptActions[menuEntry], SIGNAL(triggeredData(QString)), this, SLOT(runUserScript(QString)) );
+		menuMgr->addMenuItemString(menuEntry, "UserScripts");
+	}
+	menuMgr->addMenuItemStringstoRememberedMenu("UserScripts", scrUserScriptActions);
+}
+
 void ScripterCore::FinishScriptRun()
 {
 	ScribusMainWindow* ScMW=ScCore->primaryMainWindow();
@@ -228,6 +262,12 @@ void ScripterCore::RecentScript(const QString& fn)
 	FinishScriptRun();
 }
 
+void ScripterCore::runUserScript(const QString& fn)
+{
+	slotRunScriptFile(fn);
+	FinishScriptRun();
+}
+
 void ScripterCore::slotRunScriptFile(const QString& fileName, bool inMainInterpreter)
 {
 	slotRunScriptFile(fileName, QStringList(), inMainInterpreter);
@@ -506,6 +546,61 @@ void ScripterCore::ReadPlugPrefs()
 	m_importAllNames = prefs->getBool("importall",true);
 	m_startupScript = prefs->get("startupscript", QString::null);
 	// and have the console window set up its position
+
+	auto prefUserScriptsPath = prefs->getTable("userscriptspaths");
+	m_userScriptsPaths.clear();
+	for (int i = 0; i < prefUserScriptsPath->getRowCount(); i++)
+	{
+		m_userScriptsPaths.append(prefUserScriptsPath->get(i, 0));
+	}
+
+	m_userScriptsEnabled = !m_userScriptsPaths.empty();
+
+	if (m_userScriptsEnabled)
+	{
+		m_userScripts = getUserScripts(m_userScriptsPaths);
+	}
+}
+
+/**
+ * list all the scripts in the paths' main directory and all
+ * the main.py and subdirectory.py in the sub directories.
+ */
+QStringList ScripterCore::getUserScripts(QStringList paths)
+{
+	QStringList result{};
+	for (auto path: paths)
+	{
+		QDir dir{path};
+		if (!dir.exists())
+			continue;
+		// TODO: do we need to check for .PY?
+		for (auto scriptPath: dir.entryList(QStringList() << "*.py" << "*.PY", QDir::Files))
+		{
+
+				
+			result << dir.filePath(scriptPath);
+		}
+
+		QDirIterator dirIter(path, QDir::Dirs | QDir::NoDotAndDotDot, QDirIterator::Subdirectories);
+		while(dirIter.hasNext())
+		{
+			QDir subPath{dirIter.next()};
+			// TODO: do we need to check for .PY?
+			QStringList scriptPaths{
+				subPath.filePath(subPath.dirName() + ".py"),
+				subPath.filePath("main.py")
+			};
+			for (auto scriptPath: scriptPaths)
+			{
+				if(QFileInfo(scriptPath).exists() && !QDir(scriptPath).exists())
+				{
+					result << scriptPath;
+				}
+			}
+		}
+	}
+	return result;
 }
 
 void ScripterCore::SavePlugPrefs()
@@ -592,6 +687,10 @@ void ScripterCore::languageChange()
 	menuMgr->setText("Scripter", QObject::tr("&Script"));
 	menuMgr->setText("ScribusScripts", QObject::tr("&Scribus Scripts"));
 	menuMgr->setText("RecentScripts", QObject::tr("&Recent Scripts"));
+	if (m_userScriptsEnabled)
+	{
+		menuMgr->setText("UserScripts", QObject::tr("S&cripts"));
+	}
 }
 
 bool ScripterCore::setupMainInterpreter()
diff --git a/scribus/plugins/scriptplugin/scriptercore.h b/scribus/plugins/scriptplugin/scriptercore.h
index 8682a96adb..d9c4795b18 100644
--- a/scribus/plugins/scriptplugin/scriptercore.h
+++ b/scribus/plugins/scriptplugin/scriptercore.h
@@ -33,11 +33,13 @@ class ScripterCore : public QObject
 	QString returnString;
 	/** @brief String representation of line of code to be passed to the Python interactive interpreter */
 	QString inValue;
+	QStringList getUserScriptsPaths() { return m_userScriptsPaths; }
 
 public slots:
 	void runScriptDialog();
 	void StdScript(const QString& filebasename);
 	void RecentScript(const QString& fn);
+	void runUserScript(const QString& script);
 	void slotRunScriptFile(const QString& fileName, bool inMainInterpreter = false);
 	void slotRunScriptFile(const QString& fileName, QStringList arguments, bool inMainInterpreter = false);
 	void slotRunPythonScript(); // needed for running python script from CLI
@@ -78,9 +80,13 @@ public slots:
 	PythonConsole *pcon;
 	QStringList SavedRecentScripts;
 	QStringList RecentScripts;
+	QStringList m_userScriptsPaths;
+	bool m_userScriptsEnabled{false};
+	QStringList m_userScripts;
 	MenuManager *menuMgr;
 	QMap<QString, QPointer<ScrAction> > scrScripterActions;
 	QMap<QString, QPointer<ScrAction> > scrRecentScriptActions;
+	QMap<QString, QPointer<ScrAction> > scrUserScriptActions;
 
 	// Preferences
 	/** \brief pref: Enable access to main interpreter and 'extension scripts' */
@@ -89,6 +95,9 @@ public slots:
 	bool m_importAllNames;
 	/** \brief pref: Load this script on startup */
 	QString m_startupScript;
+private:
+	QStringList getUserScripts(QStringList paths);
+	void rebuildUserScriptsMenu();
 };
 
 #endif
diff --git a/scribus/scraction.cpp b/scribus/scraction.cpp
index 9ff8ec0cb4..dddc62eb11 100644
--- a/scribus/scraction.cpp
+++ b/scribus/scraction.cpp
@@ -114,6 +114,8 @@ void ScrAction::triggeredToTriggeredData()
 		emit triggeredData(data().toString());
 	if (m_actionType==ScrAction::RecentScript)
 		emit triggeredData(data().toString());
+	if (m_actionType==ScrAction::UserScript)
+		emit triggeredData(data().toString());
 	if (m_actionType==ScrAction::UnicodeChar)
 		emit triggeredUnicodeShortcut(data().toInt());
 	if (m_actionType==ScrAction::Layer)
@@ -143,6 +145,8 @@ void ScrAction::toggledToToggledData(bool ison)
 			emit toggledData(ison, data().toString());
 		if (m_actionType==ScrAction::RecentScript)
 			emit toggledData(ison, text());
+		if (m_actionType==ScrAction::UserScript)
+			emit toggledData(ison, text());
 		if (m_actionType==ScrAction::Layer)
 			emit toggledData(ison, data().toInt());
 		// no toggle for UnicodeChar
diff --git a/scribus/scraction.h b/scribus/scraction.h
index 0d409124a0..b526779ea9 100644
--- a/scribus/scraction.h
+++ b/scribus/scraction.h
@@ -35,7 +35,7 @@ class SCRIBUS_API ScrAction : public QAction
 	Q_OBJECT
 
 public:
-	typedef enum {Normal, DataInt, DataDouble, DataQString, RecentFile, DLL, Window, RecentScript, UnicodeChar, Layer, ActionDLL, RecentPaste, ActionDLLSE } ActionType;
+	typedef enum {Normal, DataInt, DataDouble, DataQString, RecentFile, DLL, Window, RecentScript, UserScript, UnicodeChar, Layer, ActionDLL, RecentPaste, ActionDLLSE } ActionType;
 	
 	/*!
 		\author Craig Bradney
userscripts.patch (12,207 bytes)

ale

2019-03-01 17:18

manager   ~0045945

any feedback?
anything i should improve, for a first release?

aiena

2019-03-04 07:07

reporter   ~0045952

I like the thing. If you are up for adding a "reload scripts" button next to the user scripts text box which will call the update process on user press it will be nice. But I understand your reasons for not wanting to work on it. I am ok with saving the .sla and relaunching Scribus. I think its good for the first release.

For the next release you could add "reload scripts" which refreshes the menu without a restart but again no issue.

I was wondering if there is a way to shorten the paths to the scripts displayed in action search to make searching easier. Currently the whole file paths are displayed. If we permit directories to only be added if there is no script name overlap we can abbreviate the searchable string in action_search only to the name of the script itself. So instead of

"E:\foo\bar\da\sa\ra\explodeframes.py"
"E:\foo\bar\da\sa\ra\fitframes.py"

we can just have:
explodeframes.py
fitframes.py

And searching and scanning these with our eyes is much easier.
Another idea would be to give user assigned nicknames to each added user script folder.

e.g.
<sf1>/explodeframes.py
<sf2>/explodeframes.py
<sf2>/fitframes.py

where sf1 is a user assigned nickname for "E:\foo\bar\da\sa\ra\"
and sf2 is a nickname for "C:\foo\bar\bla\"

"sf" being "script folder". These could even be auto generated too.

ale

2019-03-04 07:43

manager   ~0045954

personally, i think that the reload "button" should be in a new section, in the "scripter > scripts" dialog.

currently, i don't have the patch installed, but you are saying that in the menus you get the scripts with their paths?
iirc, it was not the case in my setup...

or you get the path only in the action search?

the idea was
- only display the name of the script, without the .py
- if two scripts have the same name, you will get the name twice.
- if you are using twice the same script name, it's up to the user to rename the script (and it's rather unlikely (and a bad habit) to have two scripts with the same name...

and if you want them to be shown differently, my idea is that one should then list all the script in a project file, which gives full control on which scripts, how and where they are listed.

but i'd like to get the current code reviewed and possibly accepted before adding further features.

aiena

2019-03-06 10:01

reporter   ~0045968

a-l-e you configure the directories only once in preferences so I feel the "refresh scripts" button should be there only for convenience so a user can press it whenever they change a path and want to force a refresh.

And sorry about the full path's thing. The action search was pulling the full paths from the "Scripter->Recent Scripts" menu which happens to display full paths. If you type the word "Recent" you will see full paths to the same scripts.

Regarding the user scripts feature -- I would recommend that you keep the .py extension I prefer that the file extensions not be stripped.

If a feature could be added to ignore displaying the recent scripts it would be nice to make things cleaner. Especially considering there is often an overlap between recently run scripts and the scripts defined in the user scripts folder. Perhaps a checkbox on by default called "do not display recently run scripts". This concept could be extended into a general purpose menu filter if people want it in the future.

The question is can a plugin add stuff to preferences without changing the Scribus main code? I feel these configuration options should be inside the plugin itself and scribus should have some API for plugins to tell Scribus what configuration options they wish to add. I wonder if its better to have a cog wheel button inside the popup action search dialog which opens its won internal preferences window only for the plugins configuration. This breaks the everything in one central place concept of preferences but it encapsulates all logic inside the main plugin.

aiena

2019-03-06 10:03

reporter   ~0045969

I think "Reload scripts" can also be in the main "Scripter" menu as "Scripter -> Reload scripts". But usually no one will reload the scripts unless they changed the config which is inside preferences so ... ?

ale

2019-03-06 14:36

manager   ~0045970

most people won't need to reload after having add a path... this is something very rarely and one can restart scribus after changing it.

but what more people will need is: reload the list after having added a new script in known path...
and, in that case, restarting scribus must be a bit more of a burden...

i think that the list should be reloaded when the path has changed, but i'm not 100% sure that i have the information... i will check it...

ale

2019-03-07 10:37

manager   ~0045974

new patch that adds

- update of the paths when preferences are saved (and the paths list has changed)
- menu entry for hot reloading the list of scripts (when you have written that cool new script)

i think that the patch is now ready for review and commit.

userscripts.diff (15,743 bytes)
diff --git a/scribus/plugins/scriptplugin/prefs_scripter.cpp b/scribus/plugins/scriptplugin/prefs_scripter.cpp
index b4bcf8094a..cf0643ad4f 100644
--- a/scribus/plugins/scriptplugin/prefs_scripter.cpp
+++ b/scribus/plugins/scriptplugin/prefs_scripter.cpp
@@ -31,6 +31,18 @@ Prefs_Scripter::Prefs_Scripter(QWidget* parent)
 	// The startup script box should be disabled  if ext scripts are off
 	startupScriptEdit->setEnabled(extensionScriptsChk->isChecked());
 	startupScriptEdit->setText(scripterCore->startupScript());
+
+	PrefsContext* prefs = PrefsManager::instance()->prefsFile->getPluginContext("scriptplugin");
+	PrefsTable* prefUserScriptsPath = prefs->getTable("userscriptspaths");
+	QStringList userPaths{};
+	for (int i = 0; i < prefUserScriptsPath->getRowCount(); i++)
+	{
+		userPaths.append(prefUserScriptsPath->get(i, 0));
+	}
+	userScriptsPaths = userPaths.join(";");
+	userScriptPathEdit->setText(userScriptsPaths);
+	userScriptPathEdit->setToolTip( "<qt>" + tr("Multiple paths are separated by semicolons. You need to restart Scribus to see new scripts.") + "</qt>");
+
 	// signals and slots connections
 	connect(extensionScriptsChk, SIGNAL(toggled(bool)), startupScriptEdit, SLOT(setEnabled(bool)));
 	// colors
@@ -79,6 +91,19 @@ void Prefs_Scripter::apply()
 		prefs->set("syntaxtext", textColor.name());
 
 		emit prefsChanged();
+
+		if (userScriptPathEdit->text() != userScriptsPaths)
+		{
+			PrefsTable* prefUserScriptsPath = prefs->getTable("userscriptspaths");
+			prefUserScriptsPath->clear();
+			int i = 0;
+			for (auto path: userScriptPathEdit->text().split(";"))
+			{
+				prefUserScriptsPath->set(i, 0, path);
+				i++;
+			}
+			emit prefsUserScriptsPathsChanged();
+		}
 	}
 }
 
diff --git a/scribus/plugins/scriptplugin/prefs_scripter.h b/scribus/plugins/scriptplugin/prefs_scripter.h
index 597236bab8..2c94d20f8b 100644
--- a/scribus/plugins/scriptplugin/prefs_scripter.h
+++ b/scribus/plugins/scriptplugin/prefs_scripter.h
@@ -37,6 +37,8 @@ class Prefs_Scripter : public Prefs_Pane, Ui::Prefs_Scripter
 		QColor signColor;
 		QColor stringColor;
 		QColor numberColor;
+	private:
+		QString userScriptsPaths{};
 
 	protected slots:
 		/*! \brief All requests for color change are handled here.
@@ -47,6 +49,7 @@ class Prefs_Scripter : public Prefs_Pane, Ui::Prefs_Scripter
 
 	signals:
 		void prefsChanged();
+		void prefsUserScriptsPathsChanged();
 };
 
 #endif // PREFS_SCRIPTER_H
diff --git a/scribus/plugins/scriptplugin/prefs_scripterbase.ui b/scribus/plugins/scriptplugin/prefs_scripterbase.ui
index 7c35b7505c..00986aca25 100644
--- a/scribus/plugins/scriptplugin/prefs_scripterbase.ui
+++ b/scribus/plugins/scriptplugin/prefs_scripterbase.ui
@@ -98,6 +98,20 @@
          </item>
         </layout>
        </item>
+       <item>
+        <layout class="QHBoxLayout" name="horizontalLayout_4">
+         <item>
+          <widget class="QLabel" name="label_3">
+           <property name="text">
+            <string>User Scripts Path:</string>
+           </property>
+          </widget>
+         </item>
+         <item>
+          <widget class="QLineEdit" name="userScriptPathEdit"/>
+         </item>
+        </layout>
+       </item>
        <item>
         <spacer name="verticalSpacer_2">
          <property name="orientation">
diff --git a/scribus/plugins/scriptplugin/scriptercore.cpp b/scribus/plugins/scriptplugin/scriptercore.cpp
index c9c8672cca..75a16fe16a 100644
--- a/scribus/plugins/scriptplugin/scriptercore.cpp
+++ b/scribus/plugins/scriptplugin/scriptercore.cpp
@@ -15,6 +15,8 @@ for which a new license (GPL+exception) is in place.
 #include <QTextCodec>
 #include <QByteArray>
 #include <QPixmap>
+#include <QDir>
+#include <QDirIterator>
 #include <cstdlib>
 
 #include "runscriptdialog.h"
@@ -48,6 +50,7 @@ ScripterCore::ScripterCore(QWidget* parent)
 	pcon = new PythonConsole(parent);
 	scrScripterActions.clear();
 	scrRecentScriptActions.clear();
+	scrUserScriptActions.clear();
 	returnString = "init";
 
 	scrScripterActions.insert("scripterExecuteScript", new ScrAction(QObject::tr("&Execute Script..."), QKeySequence(), this));
@@ -90,6 +93,7 @@ void ScripterCore::addToMainWindowMenu(ScribusMainWindow *mw)
 	menuMgr->addMenuItemString("scripterExecuteScript", "Scripter");
 	menuMgr->createMenu("RecentScripts", QObject::tr("&Recent Scripts"), "Scripter", false, true);
 	menuMgr->addMenuItemString("RecentScripts", "Scripter");
+	addUserScriptsMenu();
 	menuMgr->addMenuItemString("scripterExecuteScript", "Scripter");
 	menuMgr->addMenuItemString("SEPARATOR", "Scripter");
 	menuMgr->addMenuItemString("scripterShowConsole", "Scripter");
@@ -101,6 +105,20 @@ void ScripterCore::addToMainWindowMenu(ScribusMainWindow *mw)
 	menuMgr->addMenuItemStringstoMenuBar("Scripter", scrScripterActions);
 	RecentScripts = SavedRecentScripts;
 	rebuildRecentScriptsMenu();
+	rebuildUserScriptsMenu();
+}
+
+void ScripterCore::addUserScriptsMenu()
+{
+	if (m_userScriptsEnabled)
+	{
+		if (!menuMgr->menuExists("UserScripts"))
+		{
+			menuMgr->createMenu("UserScripts", QObject::tr("S&cripts"), "Scripter", false, true);
+			menuMgr->addMenuItemString("UserScripts", "Scripter");
+		}
+
+	}
 }
 
 void ScripterCore::enableMainWindowMenu()
@@ -109,6 +127,10 @@ void ScripterCore::enableMainWindowMenu()
 		return;
 	menuMgr->setMenuEnabled("ScribusScripts", true);
 	menuMgr->setMenuEnabled("RecentScripts", true);
+	if (m_userScriptsEnabled)
+	{
+		menuMgr->setMenuEnabled("UserScripts", true);
+	}
 	scrScripterActions["scripterExecuteScript"]->setEnabled(true);
 }
 
@@ -118,6 +140,10 @@ void ScripterCore::disableMainWindowMenu()
 		return;
 	menuMgr->setMenuEnabled("ScribusScripts", false);
 	menuMgr->setMenuEnabled("RecentScripts", false);
+	if (m_userScriptsEnabled)
+	{
+		menuMgr->setMenuEnabled("UserScripts", false);
+	}
 	scrScripterActions["scripterExecuteScript"]->setEnabled(false);
 }
 
@@ -156,6 +182,33 @@ void ScripterCore::rebuildRecentScriptsMenu()
 	menuMgr->addMenuItemStringstoRememberedMenu("RecentScripts", scrRecentScriptActions);
 }
 
+void ScripterCore::updateUserScriptsMenu()
+{
+	m_userScripts = getUserScripts(m_userScriptsPaths);
+	rebuildUserScriptsMenu();
+}
+
+void ScripterCore::rebuildUserScriptsMenu()
+{
+	menuMgr->clearMenuStrings("UserScripts");
+	scrUserScriptActions.clear();
+	for (auto script: m_userScripts)
+	{
+		QString menuEntry = QFileInfo(script).baseName();
+		scrUserScriptActions.insert(menuEntry, new ScrAction( ScrAction::RecentScript, menuEntry, QKeySequence(), this, script));
+		connect( scrUserScriptActions[menuEntry], SIGNAL(triggeredData(QString)), this, SLOT(runUserScript(QString)) );
+		menuMgr->addMenuItemString(menuEntry, "UserScripts");
+	}
+
+	menuMgr->addMenuItemString("SEPARATOR", "UserScripts");
+
+	scrUserScriptActions.insert("userScriptsUpdate", new ScrAction(QObject::tr("List &Update"), QKeySequence(), this));
+	QObject::connect( scrUserScriptActions["userScriptsUpdate"], SIGNAL(triggered()) , this, SLOT(updateUserScriptsMenu()) );
+	menuMgr->addMenuItemString("userScriptsUpdate", "UserScripts");
+
+	menuMgr->addMenuItemStringstoRememberedMenu("UserScripts", scrUserScriptActions);
+}
+
 void ScripterCore::FinishScriptRun()
 {
 	ScribusMainWindow* ScMW=ScCore->primaryMainWindow();
@@ -228,6 +281,12 @@ void ScripterCore::RecentScript(const QString& fn)
 	FinishScriptRun();
 }
 
+void ScripterCore::runUserScript(const QString& fn)
+{
+	slotRunScriptFile(fn);
+	FinishScriptRun();
+}
+
 void ScripterCore::slotRunScriptFile(const QString& fileName, bool inMainInterpreter)
 {
 	slotRunScriptFile(fileName, QStringList(), inMainInterpreter);
@@ -506,6 +565,64 @@ void ScripterCore::ReadPlugPrefs()
 	m_importAllNames = prefs->getBool("importall",true);
 	m_startupScript = prefs->get("startupscript", QString::null);
 	// and have the console window set up its position
+
+	m_userScriptsPaths = getUserScriptsPaths(prefs);
+	m_userScriptsEnabled = !m_userScriptsPaths.empty();
+	if (m_userScriptsEnabled)
+	{
+		m_userScripts = getUserScripts(m_userScriptsPaths);
+	}
+}
+
+QStringList ScripterCore::getUserScriptsPaths(PrefsContext* prefs)
+{
+	QStringList result{};
+
+	auto prefUserScriptsPath = prefs->getTable("userscriptspaths");
+	result.clear();
+	for (int i = 0; i < prefUserScriptsPath->getRowCount(); i++)
+	{
+		result.append(prefUserScriptsPath->get(i, 0));
+	}
+
+	return result;
+}
+
+QStringList ScripterCore::getUserScripts(QStringList paths)
+{
+	QStringList result{};
+	for (auto path: paths)
+	{
+		QDir dir{path};
+		if (!dir.exists())
+			continue;
+		// TODO: do we need to check for .PY?
+		for (auto scriptPath: dir.entryList(QStringList() << "*.py" << "*.PY", QDir::Files))
+		{
+
+				
+			result << dir.filePath(scriptPath);
+		}
+
+		QDirIterator dirIter(path, QDir::Dirs | QDir::NoDotAndDotDot, QDirIterator::Subdirectories);
+		while(dirIter.hasNext())
+		{
+			QDir subPath{dirIter.next()};
+			// TODO: do we need to check for .PY?
+			QStringList scriptPaths{
+				subPath.filePath(subPath.dirName() + ".py"),
+				subPath.filePath("main.py")
+			};
+			for (auto scriptPath: scriptPaths)
+			{
+				if(QFileInfo(scriptPath).exists() && !QDir(scriptPath).exists())
+				{
+					result << scriptPath;
+				}
+			}
+		}
+	}
+	return result;
 }
 
 void ScripterCore::SavePlugPrefs()
@@ -592,6 +709,10 @@ void ScripterCore::languageChange()
 	menuMgr->setText("Scripter", QObject::tr("&Script"));
 	menuMgr->setText("ScribusScripts", QObject::tr("&Scribus Scripts"));
 	menuMgr->setText("RecentScripts", QObject::tr("&Recent Scripts"));
+	if (m_userScriptsEnabled)
+	{
+		menuMgr->setText("UserScripts", QObject::tr("S&cripts"));
+	}
 }
 
 bool ScripterCore::setupMainInterpreter()
@@ -635,6 +756,18 @@ void ScripterCore::updateSyntaxHighlighter()
 	pcon->updateSyntaxHighlighter();
 }
 
+void ScripterCore::updateUserScriptsPaths()
+{
+	PrefsContext* prefs = PrefsManager::instance()->prefsFile->getPluginContext("scriptplugin");
+	m_userScriptsPaths = getUserScriptsPaths(prefs);
+	m_userScripts = getUserScripts(m_userScriptsPaths);
+	m_userScriptsEnabled = !m_userScriptsPaths.empty();
+
+	addUserScriptsMenu();
+
+	rebuildUserScriptsMenu();
+}
+
 const QString & ScripterCore::startupScript() const
 {
 	return m_startupScript;
diff --git a/scribus/plugins/scriptplugin/scriptercore.h b/scribus/plugins/scriptplugin/scriptercore.h
index 8682a96adb..7505ec218b 100644
--- a/scribus/plugins/scriptplugin/scriptercore.h
+++ b/scribus/plugins/scriptplugin/scriptercore.h
@@ -33,11 +33,14 @@ class ScripterCore : public QObject
 	QString returnString;
 	/** @brief String representation of line of code to be passed to the Python interactive interpreter */
 	QString inValue;
+	QStringList getUserScriptsPaths() { return m_userScriptsPaths; }
+	QStringList getUserScriptsPaths(PrefsContext* prefs);
 
 public slots:
 	void runScriptDialog();
 	void StdScript(const QString& filebasename);
 	void RecentScript(const QString& fn);
+	void runUserScript(const QString& script);
 	void slotRunScriptFile(const QString& fileName, bool inMainInterpreter = false);
 	void slotRunScriptFile(const QString& fileName, QStringList arguments, bool inMainInterpreter = false);
 	void slotRunPythonScript(); // needed for running python script from CLI
@@ -62,6 +65,7 @@ public slots:
 	void setStartupScript(const QString& newScript);
 	void setExtensionsEnabled(bool enable);
 	void updateSyntaxHighlighter();
+	void updateUserScriptsPaths();
 
 protected:
 	// Private helper functions
@@ -78,9 +82,13 @@ public slots:
 	PythonConsole *pcon;
 	QStringList SavedRecentScripts;
 	QStringList RecentScripts;
+	QStringList m_userScriptsPaths;
+	bool m_userScriptsEnabled{false};
+	QStringList m_userScripts;
 	MenuManager *menuMgr;
 	QMap<QString, QPointer<ScrAction> > scrScripterActions;
 	QMap<QString, QPointer<ScrAction> > scrRecentScriptActions;
+	QMap<QString, QPointer<ScrAction> > scrUserScriptActions;
 
 	// Preferences
 	/** \brief pref: Enable access to main interpreter and 'extension scripts' */
@@ -89,6 +97,17 @@ public slots:
 	bool m_importAllNames;
 	/** \brief pref: Load this script on startup */
 	QString m_startupScript;
+private slots:
+	void updateUserScriptsMenu();
+private:
+	/** \brief Add the user menu script if it does not exist yet */
+	void addUserScriptsMenu();
+	/**
+	 * \brief List all the scripts in the paths' main directory and all
+	 * the main.py and subdirectory.py in the sub directories.
+	 */
+	QStringList getUserScripts(QStringList paths);
+	void rebuildUserScriptsMenu();
 };
 
 #endif
diff --git a/scribus/plugins/scriptplugin/scriptplugin.cpp b/scribus/plugins/scriptplugin/scriptplugin.cpp
index 87795e8e87..9a0f8c0a40 100644
--- a/scribus/plugins/scriptplugin/scriptplugin.cpp
+++ b/scribus/plugins/scriptplugin/scriptplugin.cpp
@@ -205,6 +205,7 @@ bool ScriptPlugin::newPrefsPanelWidget(QWidget* parent, PrefsPanel*& panel, QStr
 	panel = new ScripterPrefsGui(parent);
 	Q_CHECK_PTR(panel);
 	connect(panel, SIGNAL(prefsChanged()), scripterCore, SLOT(updateSyntaxHighlighter()));
+	connect(panel, SIGNAL(prefsUserScriptsPathsChanged()), scripterCore, SLOT(updateUserScriptsPaths()));
 	caption = tr("Scripter");
 	icon = IconManager::instance()->loadPixmap("python.png");
 	return true;
@@ -216,6 +217,7 @@ bool ScriptPlugin::newPrefsPanelWidget(QWidget* parent, Prefs_Pane*& panel, QStr
 	panel = new Prefs_Scripter(parent);
 	Q_CHECK_PTR(panel);
 	connect(panel, SIGNAL(prefsChanged()), scripterCore, SLOT(updateSyntaxHighlighter()));
+	connect(panel, SIGNAL(prefsUserScriptsPathsChanged()), scripterCore, SLOT(updateUserScriptsPaths()));
 	caption = tr("Scripter");
 	icon = IconManager::instance()->loadPixmap("python_16.png");
 	return true;
diff --git a/scribus/plugins/scriptplugin/scriptplugin.h b/scribus/plugins/scriptplugin/scriptplugin.h
index 6f6bdfce72..1983e39629 100644
--- a/scribus/plugins/scriptplugin/scriptplugin.h
+++ b/scribus/plugins/scriptplugin/scriptplugin.h
@@ -41,6 +41,8 @@ class PLUGIN_API ScriptPlugin : public ScPersistentPlugin
 
 		// Special features (none)
 		QByteArray pythonHome;
+		private slots:
+			void updateUserScriptsPaths();
 };
 
 extern "C" PLUGIN_API int scriptplugin_getPluginAPIVersion();
diff --git a/scribus/scraction.cpp b/scribus/scraction.cpp
index 9ff8ec0cb4..dddc62eb11 100644
--- a/scribus/scraction.cpp
+++ b/scribus/scraction.cpp
@@ -114,6 +114,8 @@ void ScrAction::triggeredToTriggeredData()
 		emit triggeredData(data().toString());
 	if (m_actionType==ScrAction::RecentScript)
 		emit triggeredData(data().toString());
+	if (m_actionType==ScrAction::UserScript)
+		emit triggeredData(data().toString());
 	if (m_actionType==ScrAction::UnicodeChar)
 		emit triggeredUnicodeShortcut(data().toInt());
 	if (m_actionType==ScrAction::Layer)
@@ -143,6 +145,8 @@ void ScrAction::toggledToToggledData(bool ison)
 			emit toggledData(ison, data().toString());
 		if (m_actionType==ScrAction::RecentScript)
 			emit toggledData(ison, text());
+		if (m_actionType==ScrAction::UserScript)
+			emit toggledData(ison, text());
 		if (m_actionType==ScrAction::Layer)
 			emit toggledData(ison, data().toInt());
 		// no toggle for UnicodeChar
diff --git a/scribus/scraction.h b/scribus/scraction.h
index 0d409124a0..b526779ea9 100644
--- a/scribus/scraction.h
+++ b/scribus/scraction.h
@@ -35,7 +35,7 @@ class SCRIBUS_API ScrAction : public QAction
 	Q_OBJECT
 
 public:
-	typedef enum {Normal, DataInt, DataDouble, DataQString, RecentFile, DLL, Window, RecentScript, UnicodeChar, Layer, ActionDLL, RecentPaste, ActionDLLSE } ActionType;
+	typedef enum {Normal, DataInt, DataDouble, DataQString, RecentFile, DLL, Window, RecentScript, UserScript, UnicodeChar, Layer, ActionDLL, RecentPaste, ActionDLLSE } ActionType;
 	
 	/*!
 		\author Craig Bradney
userscripts.diff (15,743 bytes)

ale

2019-03-07 11:21

manager   ~0045975

the "Scripts" menu entry is not created when one adds the first path.

the code gets to the code adding the menu

https://github.com/aoloe/scribus/blob/userscripts/scribus/plugins/scriptplugin/scriptercore.cpp#L117

but the menu does not show up.

looks like a restriction of Qt or of the way menumanager works...

imo it's ok to commit the patch as is, but if someone finds a solution, i'm more than happy : - )

Issue History

Date Modified Username Field Change
2019-02-20 08:31 ale New Issue
2019-02-21 14:09 aiena Note Added: 0045908
2019-02-21 14:23 ale Note Added: 0045909
2019-02-24 06:34 aiena Note Added: 0045917
2019-02-26 08:29 ale Note Added: 0045924
2019-02-26 08:30 ale Note Added: 0045925
2019-02-28 12:34 ale Note Added: 0045938
2019-02-28 12:49 ale Note Added: 0045939
2019-02-28 17:33 ale File Added: userscripts.patch
2019-02-28 17:33 ale Note Added: 0045940
2019-03-01 17:18 ale Note Added: 0045945
2019-03-04 07:07 aiena Note Added: 0045952
2019-03-04 07:43 ale Note Added: 0045954
2019-03-06 10:01 aiena Note Added: 0045968
2019-03-06 10:03 aiena Note Added: 0045969
2019-03-06 14:36 ale Note Added: 0045970
2019-03-07 10:37 ale File Added: userscripts.diff
2019-03-07 10:37 ale Note Added: 0045974
2019-03-07 10:38 ale Summary add user's scripts to the navigation => [PATCH] add user's scripts to the navigation
2019-03-07 10:38 ale Patch No => Yes
2019-03-07 10:38 ale Tag Attached: patch
2019-03-07 11:21 ale Note Added: 0045975