Browse Source

A simple script based plugin system

Neal Wilson 7 years ago
parent
commit
34c38435a4
2 changed files with 183 additions and 5 deletions
  1. 162
    0
      src/plugins.w
  2. 21
    5
      src/typica.w

+ 162
- 0
src/plugins.w View File

@@ -0,0 +1,162 @@
1
+@** Simple Plugins.
2
+
3
+\noindent The original motivation for this feature is to provide a simple way
4
+to allow importing data from other data logging applications. The problem is
5
+that there are huge differences in the data formats exported by different
6
+applications, sometimes there are differences that depend on how the other
7
+application was configured which cannot be reliably determined in an automated
8
+fashion, and if a substantial number of import plugins were created, any given
9
+person using Typica would be unlikely to ever use most of them.
10
+
11
+Based on these concerns, I wanted something that would make it easy to create
12
+new import plugins without the need to create a new build of Typica every time,
13
+I wanted it to be relatively easy for people to modify example import plugins
14
+to suit the data they wanted to import, and I wanted it to be easy for people
15
+to hide plugins that they were not required.
16
+
17
+This is handled in a way similar to reports. A new directory is provided with
18
+the \pn{} configuration which contains files with script code. A menu item is
19
+available that will examine the files in that folder to populate its sub-menu.
20
+
21
+@<Process plugin item@>=
22
+QMenu *pluginMenu = new QMenu(menu);
23
+if(itemElement.hasAttribute("id"))
24
+{
25
+	pluginMenu->setObjectName(itemElement.attribute("id"));
26
+}
27
+if(itemElement.hasAttribute("title"))
28
+{
29
+	pluginMenu->setTitle(itemElement.attribute("title"));
30
+}
31
+if(itemElement.hasAttribute("src"))
32
+{
33
+	QSettings settings;
34
+	QString pluginDirectory = QString("%1/%2").
35
+		arg(settings.value("config").toString()).
36
+		arg(itemElement.attribute("src"));
37
+	QDir directory(pluginDirectory);
38
+	directory.setFilter(QDir::Files);
39
+	directory.setSorting(QDir::Name);
40
+	QStringList nameFilter;
41
+	nameFilter << "*.js";
42
+	directory.setNameFilters(nameFilter);
43
+	QFileInfoList pluginFiles = directory.entryInfoList();
44
+	for(int k = 0; k < pluginFiles.size(); k++)
45
+	{
46
+		PluginAction *pa = new PluginAction(pluginFiles.at(k), pluginMenu);
47
+		if(itemElement.hasAttribute("preRun"))
48
+		{
49
+			pa->setPreRun(itemElement.attribute("preRun"));
50
+		}
51
+		if(itemElement.hasAttribute("postRun"))
52
+		{
53
+			pa->setPostRun(itemElement.attribute("postRun"));
54
+		}
55
+		pluginMenu->addAction(pa);
56
+	}
57
+}
58
+menu->addMenu(pluginMenu);
59
+
60
+@ The sub-menu items are a subclass of |QAction| which holds all of the
61
+information needed to respond to its activation.
62
+
63
+@<Class declarations@>=
64
+class PluginAction : public QAction
65
+{
66
+	Q_OBJECT
67
+	Q_PROPERTY(QString preRun READ preRun WRITE setPreRun);
68
+	Q_PROPERTY(QString postRun READ postRun WRITE setPostRun);
69
+	public:
70
+		PluginAction(const QFileInfo &info, QObject *parent);
71
+		QString preRun();
72
+		QString postRun();
73
+	public slots:
74
+		void setPreRun(const QString &script);
75
+		void setPostRun(const QString &script);
76
+	private slots:
77
+		void runScript();
78
+	private:
79
+		QString pluginFile;
80
+		QString preRunScript;
81
+		QString postRunScript;
82
+};
83
+
84
+@ The constructor takes a |QFileInfo| and uses that to extract the path of the
85
+file used to respond to the action activation as well as the text that should
86
+be used in the menu text. It also takes a |QObject*| parent which should be the
87
+|QMenu| the |PluginAction| will be placed in.
88
+
89
+Everything interesting happens in |runScript()| which is called when the action
90
+is triggered.
91
+
92
+@<PluginAction implementation@>=
93
+PluginAction::PluginAction(const QFileInfo &info, QObject *parent) :
94
+	QAction(parent), preRunScript(""), postRunScript("")
95
+{
96
+	pluginFile = info.absoluteFilePath();
97
+	setText(info.baseName());
98
+	connect(this, SIGNAL(triggered()), this, SLOT(runScript()));
99
+}
100
+
101
+void PluginAction::runScript()
102
+{
103
+	QFile file(pluginFile);
104
+	if(file.open(QIODevice::ReadOnly))
105
+	{
106
+		QScriptEngine *engine = AppInstance->engine;
107
+		QScriptContext *context = engine->pushContext();
108
+		if(parent()->dynamicPropertyNames().contains("activationObject"))
109
+		{
110
+			QScriptValue activationObject =
111
+				parent()->property("activationObject").value<QScriptValue>();
112
+			context->setActivationObject(activationObject);
113
+		}
114
+		QString script(file.readAll());
115
+		QScriptValue retval = engine->evaluate(preRunScript + script + postRunScript, pluginFile);
116
+		if(engine->hasUncaughtException())
117
+		{
118
+			qDebug() << "Uncaught exception: " <<
119
+				engine->uncaughtException().toString() <<
120
+				" in " << pluginFile << " line: " <<
121
+				engine->uncaughtExceptionLineNumber();
122
+		}
123
+		engine->popContext();
124
+		file.close();
125
+	}
126
+}
127
+
128
+@ Pre-run and post-run scripts can be set to handle boilerplate that would
129
+otherwise need to be included in all plugins.
130
+
131
+@<PluginAction implementation@>=
132
+QString PluginAction::preRun()
133
+{
134
+	return preRunScript;
135
+}
136
+
137
+QString PluginAction::postRun()
138
+{
139
+	return postRunScript;
140
+}
141
+
142
+void PluginAction::setPreRun(const QString &script)
143
+{
144
+	preRunScript = script;
145
+}
146
+
147
+void PluginAction::setPostRun(const QString &script)
148
+{
149
+	postRunScript = script;
150
+}
151
+
152
+@ In order to get the activation object in this way, we need to allow a
153
+|QScriptValue| to be stored in a |QVariant|.
154
+
155
+@<Class declarations@>=
156
+Q_DECLARE_METATYPE(QScriptValue)
157
+
158
+@ This is added to the list of class implementations.
159
+
160
+@<Class implementations@>=
161
+@<PluginAction implementation@>
162
+

+ 21
- 5
src/typica.w View File

@@ -810,15 +810,25 @@ The first of these is |QObject|.
810 810
 
811 811
 @<Function prototypes for scripting@>=
812 812
 void setQObjectProperties(QScriptValue value, QScriptEngine *engine);
813
+QScriptValue QObject_setProperty(QScriptContext *context, QScriptEngine *engine);
813 814
 
814
-@ As there are no properties that need to be set for this class and as this
815
-class does not inherit any other class, nothing needs to be done in this method.
816
-It will, however, be called by subclasses in case this changes in the future.
815
+@ Attaching properties to a |QScriptValue| that wraps a |QObject| does not
816
+create a dynamic property on the underlying |QObject| by default. This can
817
+cause issues with certain interactions between script and native code. Rather
818
+than change every wrapper, we can instead expose a |setProperty()| method.
817 819
 
818 820
 @<Functions for scripting@>=
819
-void setQObjectProperties(QScriptValue, QScriptEngine *)
821
+void setQObjectProperties(QScriptValue value, QScriptEngine *engine)
820 822
 {
821
-    /* Nothing needs to be done here. */
823
+    value.setProperty("setProperty", engine->newFunction(QObject_setProperty));
824
+}
825
+
826
+QScriptValue QObject_setProperty(QScriptContext *context, QScriptEngine *)
827
+{
828
+	QObject *self = getself<QObject *>(context);
829
+	self->setProperty(argument<QString>(0, context).toUtf8().constData(),
830
+	                  argument<QVariant>(1, context));
831
+    return QScriptValue();
822 832
 }
823 833
 
824 834
 @ The same can be done for |QPaintDevice| and |QLayoutItem|.
@@ -4804,6 +4814,10 @@ while(j < menuItems.count())
4804 4814
         {
4805 4815
             menu->addSeparator();
4806 4816
         }
4817
+        else if(itemElement.tagName() == "plugins")
4818
+        {
4819
+	        @<Process plugin item@>@;
4820
+        }
4807 4821
     }
4808 4822
     j++;
4809 4823
 }
@@ -14467,6 +14481,8 @@ void setQTextEditProperties(QScriptValue value, QScriptEngine *engine)
14467 14481
     value.setProperty("print", engine->newFunction(QTextEdit_print));
14468 14482
 }
14469 14483
 
14484
+@i plugins.w
14485
+
14470 14486
 @i daterangeselector.w
14471 14487
 
14472 14488
 @** An area for repeated user interface elements.

Loading…
Cancel
Save