1 ///////////////////////////////////////////////////////////////////////////////
2 // BSD 3-Clause License
3 //
4 // Copyright (c) 2019, The Regents of the University of California
5 // All rights reserved.
6 //
7 // Redistribution and use in source and binary forms, with or without
8 // modification, are permitted provided that the following conditions are met:
9 //
10 // * Redistributions of source code must retain the above copyright notice, this
11 // list of conditions and the following disclaimer.
12 //
13 // * Redistributions in binary form must reproduce the above copyright notice,
14 // this list of conditions and the following disclaimer in the documentation
15 // and/or other materials provided with the distribution.
16 //
17 // * Neither the name of the copyright holder nor the names of its
18 // contributors may be used to endorse or promote products derived from
19 // this software without specific prior written permission.
20 //
21 // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
22 // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
23 // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
24 // ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
25 // LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
26 // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
27 // SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
28 // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
29 // CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
30 // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
31 // POSSIBILITY OF SUCH DAMAGE.
32
33 #include "scriptWidget.h"
34
35 #include <errno.h>
36 #include <unistd.h>
37
38 #include <QCoreApplication>
39 #include <QHBoxLayout>
40 #include <QKeyEvent>
41 #include <QTimer>
42 #include <QVBoxLayout>
43
44 #include "ord/OpenRoad.hh"
45 #include "utl/Logger.h"
46 #include "spdlog/formatter.h"
47 #include "spdlog/sinks/base_sink.h"
48
49 namespace gui {
50
ScriptWidget(QWidget * parent)51 ScriptWidget::ScriptWidget(QWidget* parent)
52 : QDockWidget("Scripting", parent),
53 output_(new QTextEdit),
54 input_(new TclCmdInputWidget),
55 pauser_(new QPushButton("Idle")),
56 historyPosition_(0)
57 {
58 setObjectName("scripting"); // for settings
59
60 output_->setReadOnly(true);
61 pauser_->setEnabled(false);
62 pauser_->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Expanding);
63
64 QHBoxLayout* inner_layout = new QHBoxLayout;
65 inner_layout->addWidget(pauser_);
66 inner_layout->addWidget(input_);
67
68 QVBoxLayout* layout = new QVBoxLayout;
69 layout->addWidget(output_, /* stretch */ 1);
70 layout->addLayout(inner_layout);
71
72 QWidget* container = new QWidget;
73 container->setLayout(layout);
74
75 QTimer::singleShot(200, this, &ScriptWidget::setupTcl);
76
77 connect(input_, SIGNAL(completeCommand()), this, SLOT(executeCommand()));
78 connect(this, SIGNAL(commandExecuted(int)), input_, SLOT(commandExecuted(int)));
79 connect(input_, SIGNAL(historyGoBack()), this, SLOT(goBackHistory()));
80 connect(input_, SIGNAL(historyGoForward()), this, SLOT(goForwardHistory()));
81 connect(input_, SIGNAL(textChanged()), this, SLOT(outputChanged()));
82 connect(output_, SIGNAL(textChanged()), this, SLOT(outputChanged()));
83 connect(pauser_, SIGNAL(pressed()), this, SLOT(pauserClicked()));
84
85 setWidget(container);
86 }
87
channelClose(ClientData instance_data,Tcl_Interp * interp)88 int channelClose(ClientData instance_data, Tcl_Interp* interp)
89 {
90 // This channel should never be closed
91 return EINVAL;
92 }
93
channelOutput(ClientData instance_data,const char * buf,int to_write,int * error_code)94 int ScriptWidget::channelOutput(ClientData instance_data,
95 const char* buf,
96 int to_write,
97 int* error_code)
98 {
99 // Buffer up the output
100 ScriptWidget* widget = (ScriptWidget*) instance_data;
101 widget->logger_->report(std::string(buf, to_write));
102 return to_write;
103 }
104
channelWatch(ClientData instance_data,int mask)105 void channelWatch(ClientData instance_data, int mask)
106 {
107 // watch is not supported inside OpenROAD GUI
108 }
109
110 Tcl_ChannelType ScriptWidget::stdout_channel_type_ = {
111 // Tcl stupidly defines this a non-cost char*
112 ((char*) "stdout_channel"), /* typeName */
113 TCL_CHANNEL_VERSION_2, /* version */
114 channelClose, /* closeProc */
115 nullptr, /* inputProc */
116 ScriptWidget::channelOutput, /* outputProc */
117 nullptr, /* seekProc */
118 nullptr, /* setOptionProc */
119 nullptr, /* getOptionProc */
120 channelWatch, /* watchProc */
121 nullptr, /* getHandleProc */
122 nullptr, /* close2Proc */
123 nullptr, /* blockModeProc */
124 nullptr, /* flushProc */
125 nullptr, /* handlerProc */
126 nullptr, /* wideSeekProc */
127 nullptr, /* threadActionProc */
128 nullptr /* truncateProc */
129 };
130
tclExitHandler(ClientData instance_data,Tcl_Interp * interp,int argc,const char ** argv)131 int ScriptWidget::tclExitHandler(ClientData instance_data,
132 Tcl_Interp *interp,
133 int argc,
134 const char **argv) {
135 ScriptWidget* widget = (ScriptWidget*) instance_data;
136 // announces exit to Qt
137 emit widget->tclExiting();
138 // does not matter from here on, since GUI is getting ready exit
139 return TCL_OK;
140 }
141
setupTcl()142 void ScriptWidget::setupTcl()
143 {
144 interp_ = Tcl_CreateInterp();
145
146 Tcl_Channel stdout_channel = Tcl_CreateChannel(
147 &stdout_channel_type_, "stdout", (ClientData) this, TCL_WRITABLE);
148 if (stdout_channel) {
149 Tcl_SetChannelOption(nullptr, stdout_channel, "-translation", "lf");
150 Tcl_SetChannelOption(nullptr, stdout_channel, "-buffering", "none");
151 Tcl_RegisterChannel(interp_, stdout_channel); // per man page: some tcl bug
152 Tcl_SetStdChannel(stdout_channel, TCL_STDOUT);
153 }
154
155 // Overwrite exit to allow Qt to handle exit
156 Tcl_CreateCommand(interp_, "exit", ScriptWidget::tclExitHandler, this, nullptr);
157
158 // Ensures no newlines are present in stdout stream when using logger, but normal behavior in file writing
159 Tcl_Eval(interp_, "rename puts ::tcl::openroad::puts");
160 Tcl_Eval(interp_, "proc puts { args } { if {[llength $args] == 1} { ::tcl::openroad::puts -nonewline {*}$args } else { ::tcl::openroad::puts {*}$args } }");
161
162 pauser_->setText("Running");
163 pauser_->setStyleSheet("background-color: red");
164 ord::tclAppInit(interp_);
165 pauser_->setText("Idle");
166 pauser_->setStyleSheet("");
167
168 // TODO: tclAppInit should return the status which we could
169 // pass to updateOutput
170 addTclResultToOutput(TCL_OK);
171
172 input_->init(interp_);
173 }
174
executeCommand()175 void ScriptWidget::executeCommand()
176 {
177 pauser_->setText("Running");
178 pauser_->setStyleSheet("background-color: red");
179 QString command = input_->text();
180
181 // Show the command that we executed
182 addCommandToOutput(command);
183
184 int return_code = Tcl_Eval(interp_, command.toLatin1().data());
185
186 // Show its output
187 addTclResultToOutput(return_code);
188
189 if (return_code == TCL_OK) {
190 // record the successful command to tcl history command
191 Tcl_RecordAndEval(interp_, command.toLatin1().data(), TCL_NO_EVAL);
192
193 // Update history; ignore repeated commands and keep last 100
194 const int history_limit = 100;
195 if (history_.empty() || command != history_.last()) {
196 if (history_.size() == history_limit) {
197 history_.pop_front();
198 }
199
200 history_.append(command);
201 }
202 historyPosition_ = history_.size();
203 }
204
205 pauser_->setText("Idle");
206 pauser_->setStyleSheet("");
207
208 emit commandExecuted(return_code);
209 }
210
addCommandToOutput(const QString & cmd)211 void ScriptWidget::addCommandToOutput(const QString& cmd)
212 {
213 const QString first_line_prefix = ">>> ";
214 const QString continue_line_prefix = "... ";
215
216 QString command = first_line_prefix + cmd;
217 command.replace("\n", "\n" + continue_line_prefix);
218
219 addToOutput(command, cmd_msg_);
220 }
221
addTclResultToOutput(int return_code)222 void ScriptWidget::addTclResultToOutput(int return_code)
223 {
224 // Show the return value color-coded by ok/err.
225 const char* result = Tcl_GetString(Tcl_GetObjResult(interp_));
226 if (result[0] != '\0') {
227 addToOutput(result, (return_code == TCL_OK) ? tcl_ok_msg_ : tcl_error_msg_);
228 }
229 }
230
addLogToOutput(const QString & text,const QColor & color)231 void ScriptWidget::addLogToOutput(const QString& text, const QColor& color)
232 {
233 addToOutput(text, color);
234 }
235
addReportToOutput(const QString & text)236 void ScriptWidget::addReportToOutput(const QString& text)
237 {
238 addToOutput(text, buffer_msg_);
239 }
240
addToOutput(const QString & text,const QColor & color)241 void ScriptWidget::addToOutput(const QString& text, const QColor& color)
242 {
243 // make sure cursor is at the end of the document
244 output_->moveCursor(QTextCursor::End);
245
246 QString output_text = text;
247 if (text.endsWith('\n')) {
248 // remove last new line
249 output_text.chop(1);
250 }
251
252 // set new text color
253 output_->setTextColor(color);
254
255 QStringList output;
256 for (QString& text_line : output_text.split('\n')) {
257 // check for line length limits
258 if (text_line.size() > max_output_line_length_) {
259 text_line = text_line.left(max_output_line_length_-3);
260 text_line += "...";
261 }
262
263 output.append(text_line);
264 }
265 // output new text
266 output_->append(output.join("\n"));
267
268 // ensure changes are updated
269 QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
270 }
271
~ScriptWidget()272 ScriptWidget::~ScriptWidget()
273 {
274 // TODO: I am being lazy and not cleaning up the tcl interpreter.
275 // We are likely exiting anyways
276 }
277
goForwardHistory()278 void ScriptWidget::goForwardHistory()
279 {
280 if (historyPosition_ < history_.size() - 1) {
281 ++historyPosition_;
282 input_->setText(history_[historyPosition_]);
283 } else if (historyPosition_ == history_.size() - 1) {
284 ++historyPosition_;
285 input_->setText(history_buffer_last_);
286 }
287 }
288
goBackHistory()289 void ScriptWidget::goBackHistory()
290 {
291 if (historyPosition_ > 0) {
292 if (historyPosition_ == history_.size()) {
293 // whats in the buffer is the last thing the user was editing
294 history_buffer_last_ = input_->text();
295 }
296 --historyPosition_;
297 input_->setText(history_[historyPosition_]);
298 }
299 }
300
readSettings(QSettings * settings)301 void ScriptWidget::readSettings(QSettings* settings)
302 {
303 settings->beginGroup("scripting");
304 history_ = settings->value("history").toStringList();
305 historyPosition_ = history_.size();
306
307 input_->readSettings(settings);
308
309 settings->endGroup();
310 }
311
writeSettings(QSettings * settings)312 void ScriptWidget::writeSettings(QSettings* settings)
313 {
314 settings->beginGroup("scripting");
315 settings->setValue("history", history_);
316
317 input_->writeSettings(settings);
318
319 settings->endGroup();
320 }
321
pause()322 void ScriptWidget::pause()
323 {
324 QString prior_text = pauser_->text();
325 bool prior_enable = pauser_->isEnabled();
326 QString prior_style = pauser_->styleSheet();
327 pauser_->setText("Continue");
328 pauser_->setStyleSheet("background-color: yellow");
329 pauser_->setEnabled(true);
330 paused_ = true;
331
332 // Keep processing events until the user continues
333 while (paused_) {
334 QCoreApplication::processEvents(QEventLoop::WaitForMoreEvents);
335 }
336
337 pauser_->setText(prior_text);
338 pauser_->setStyleSheet(prior_style);
339 pauser_->setEnabled(prior_enable);
340
341 // Make changes visible while command runs
342 QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
343 }
344
pauserClicked()345 void ScriptWidget::pauserClicked()
346 {
347 paused_ = false;
348 }
349
outputChanged()350 void ScriptWidget::outputChanged()
351 {
352 // ensure the new output is visible
353 output_->ensureCursorVisible();
354 // Make changes visible
355 QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
356 }
357
resizeEvent(QResizeEvent * event)358 void ScriptWidget::resizeEvent(QResizeEvent* event)
359 {
360 input_->setMaximumHeight(event->size().height() - output_->sizeHint().height());
361 QDockWidget::resizeEvent(event);
362 }
363
setFont(const QFont & font)364 void ScriptWidget::setFont(const QFont& font)
365 {
366 QDockWidget::setFont(font);
367 output_->setFont(font);
368 input_->setFont(font);
369 }
370
371 // This class is an spdlog sink that writes the messages into the output
372 // area.
373 template <typename Mutex>
374 class ScriptWidget::GuiSink : public spdlog::sinks::base_sink<Mutex>
375 {
376 public:
GuiSink(ScriptWidget * widget)377 GuiSink(ScriptWidget* widget) : widget_(widget) {}
378
379 protected:
sink_it_(const spdlog::details::log_msg & msg)380 void sink_it_(const spdlog::details::log_msg& msg) override
381 {
382 // Convert the msg into a formatted string
383 spdlog::memory_buf_t formatted;
384 this->formatter_->format(msg, formatted);
385 std::string formatted_msg = std::string(formatted.data(), formatted.size());
386
387 if (msg.level == spdlog::level::level_enum::off) {
388 // this comes from a ->report
389 widget_->addReportToOutput(formatted_msg.c_str());
390 }
391 else {
392 // select error message color if message level is error or above.
393 const QColor& msg_color = msg.level >= spdlog::level::level_enum::err ? widget_->tcl_error_msg_ : widget_->buffer_msg_;
394
395 widget_->addLogToOutput(formatted_msg.c_str(), msg_color);
396 }
397 }
398
flush_()399 void flush_() override {}
400
401 private:
402 ScriptWidget* widget_;
403 };
404
setLogger(utl::Logger * logger)405 void ScriptWidget::setLogger(utl::Logger* logger)
406 {
407 logger_ = logger;
408 logger->addSink(std::make_shared<GuiSink<std::mutex>>(this));
409 }
410
411 } // namespace gui
412