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