1 /****************************************************************************************
2  * Copyright (c) 2013 Anmol Ahuja <darthcodus@gmail.com>                                *
3  *                                                                                      *
4  * This program is free software; you can redistribute it and/or modify it under        *
5  * the terms of the GNU General Public License as published by the Free Software        *
6  * Foundation; either version 2 of the License, or (at your option) any later           *
7  * version.                                                                             *
8  *                                                                                      *
9  * This program is distributed in the hope that it will be useful, but WITHOUT ANY      *
10  * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A      *
11  * PARTICULAR PURPOSE. See the GNU General Public License for more details.             *
12  *                                                                                      *
13  * You should have received a copy of the GNU General Public License along with         *
14  * this program.  If not, see <http://www.gnu.org/licenses/>.                           *
15  ****************************************************************************************/
16 
17 #include "ScriptConsole.h"
18 #define DEBUG_PREFIX "ScriptConsole"
19 
20 #include "core/support/Amarok.h"
21 #include "core/support/Debug.h"
22 #include "MainWindow.h"
23 #include "ScriptEditorDocument.h"
24 #include "ScriptConsoleItem.h"
25 
26 #include <QAction>
27 #include <QApplication>
28 #include <QFileDialog>
29 #include <QListWidget>
30 #include <QKeyEvent>
31 #include <QMenuBar>
32 #include <QJSEngine>
33 #include <QSettings>
34 #include <QStandardPaths>
35 #include <QTemporaryFile>
36 #include <QToolBar>
37 
38 #include <KMessageBox>
39 #include <KTextEditor/Editor>
40 #include <KTextEditor/View>
41 #include <KLocalizedString>
42 
43 #include <iostream>
44 
45 using namespace AmarokScript;
46 using namespace ScriptConsoleNS;
47 
48 QPointer<ScriptConsole> ScriptConsole::s_instance;
49 
50 ScriptConsole*
instance()51 ScriptConsole::instance()
52 {
53     if( !s_instance )
54         s_instance = new ScriptConsole( The::mainWindow() );
55     return s_instance.data();
56 }
57 
58 //private
59 
ScriptConsole(QWidget * parent)60 ScriptConsole::ScriptConsole( QWidget *parent )
61     : QMainWindow( parent, Qt::Window )
62 {
63     m_editor = KTextEditor::Editor::instance();
64     if ( !m_editor )
65     {
66         KMessageBox::error( nullptr, i18n("A KDE text-editor component could not be found.\n"
67                                    "Please check your KDE installation.  Exiting the console!") );
68         deleteLater();
69         return;
70     }
71 
72     setDockNestingEnabled( true );
73     setWindowTitle( i18n( "Script Console" ) );
74     setObjectName( QStringLiteral("scriptconsole") );
75 
76     m_scriptListDock = new ScriptListDockWidget( this );
77     m_codeWidget = getWidget( i18n("Code"), nullptr );
78     m_consoleWidget = getWidget( i18n("Console"), nullptr );
79     m_outputWidget = getWidget( i18n( "Output" ), nullptr );
80     m_errorWidget = getWidget( i18n( "Error" ), nullptr );
81 
82     QList<QDockWidget*> debugWidgets = QList<QDockWidget*>()
83                     << m_codeWidget
84                     << m_consoleWidget
85                     << m_outputWidget
86                     << m_errorWidget;
87     foreach( QDockWidget *widget, debugWidgets )
88     {
89       addDockWidget( Qt::BottomDockWidgetArea, widget );
90     }
91     tabifyDockWidget( debugWidgets[1], debugWidgets[2] );
92     tabifyDockWidget( debugWidgets[2], debugWidgets[3] );
93 
94     addDockWidget( Qt::BottomDockWidgetArea, m_scriptListDock );
95 
96     QMenuBar *bar = new QMenuBar( this );
97     setMenuBar( bar );
98     QToolBar *toolBar = new QToolBar( this );
99     QAction *action = new QAction( i18n( "Stop" ), this );
100     action->setIcon( QApplication::style()->standardIcon( QStyle::SP_MediaStop ) );
101     connect( action, &QAction::toggled, this, &ScriptConsole::slotAbortEvaluation );
102     toolBar->addAction( action );
103     action = new QAction( QIcon::fromTheme( QStringLiteral("media-playback-start") ), i18n("Execute Script"), this );
104     action->setShortcut( Qt::CTRL + Qt::Key_Enter );
105     connect( action, &QAction::triggered, this, &ScriptConsole::slotExecuteNewScript );
106     toolBar->addAction( action );
107     action = new QAction( QIcon::fromTheme( QStringLiteral("document-new") ), i18n( "&New Script" ), this );
108     action->setShortcut( Qt::CTRL + Qt::Key_N );
109     toolBar->addAction( action );
110     connect( action, &QAction::triggered, this, &ScriptConsole::slotNewScript );
111     action = new QAction( QIcon::fromTheme( QStringLiteral("edit-delete") ), i18n( "&Delete Script" ), this );
112     toolBar->addAction( action );
113     connect( action, &QAction::triggered, m_scriptListDock, &ScriptListDockWidget::removeCurrentScript );
114     action = new QAction( i18n( "&Clear All Scripts" ), this );
115     toolBar->addAction( action );
116     connect( action, &QAction::triggered, m_scriptListDock, &ScriptListDockWidget::clear );
117     action = new QAction( i18n("Previous Script"), this );
118     action->setShortcut( QKeySequence::MoveToPreviousPage );
119     connect( action, &QAction::triggered, m_scriptListDock, &ScriptListDockWidget::prev );
120     toolBar->addAction( action );
121     action = new QAction( i18n("Next Script"), this );
122     action->setShortcut( QKeySequence::MoveToNextPage );
123     connect( action, &QAction::triggered, m_scriptListDock, &ScriptListDockWidget::next );
124     toolBar->addAction( action );
125 
126     addToolBar( toolBar );
127 
128     QMenu *viewMenu = new QMenu( this );
129     viewMenu->setTitle( i18n( "&View" ) );
130     foreach( QDockWidget *dockWidget, findChildren<QDockWidget*>() )
131     {
132         if( dockWidget->parentWidget() == this )
133             viewMenu->addAction( dockWidget->toggleViewAction() );
134     }
135     menuBar()->addMenu( viewMenu );
136 
137     addDockWidget( Qt::BottomDockWidgetArea, m_scriptListDock );
138     connect( m_scriptListDock, &ScriptListDockWidget::edit, this, &ScriptConsole::slotEditScript );
139     connect( m_scriptListDock, &ScriptListDockWidget::currentItemChanged, this, &ScriptConsole::setCurrentScriptItem );
140 
141     QListWidgetItem *item = new QListWidgetItem( "The Amarok Script Console allows you to easily execute"
142                                                 "JavaScript with access to all functions\nand methods you would"
143                                                 "have in an Amarok script.\nInformation on scripting for Amarok is"
144                                                 "available at:\nhttp://community.kde.org/Amarok/Development#Scripting"
145                                                 "\nExecute code: CTRL-Enter\nBack in code history: Page Up"
146                                                 "\nForward in code history: Page Down"
147                                                , 0 );
148     item->setFlags( Qt::NoItemFlags );
149     m_scriptListDock->addItem( item );
150 
151     QSettings settings( QStringLiteral("KDE"), QStringLiteral("Amarok") );
152     settings.beginGroup( QStringLiteral("ScriptConsole") );
153     restoreGeometry( settings.value(QStringLiteral("geometry")).toByteArray() );
154     m_savePath = settings.value(QStringLiteral("savepath")).toString();
155     settings.endGroup();
156 
157     if( m_savePath.isEmpty() )
158         m_savePath = Amarok::saveLocation(QStringLiteral("scriptconsole"));
159 
160     slotNewScript();
161     show();
162     raise();
163 
164     // Install interceptor for JS console logs and forward to appropriate widget
165     qInstallMessageHandler( [] ( QtMsgType type, const QMessageLogContext &context, const QString &msg )
166     {
167         Q_UNUSED( type );
168         QString category(context.category);
169         if ( category.compare( "js" ) == 0 ) {
170 
171             QString scriptName( context.file );
172             // clean "file:" from file name
173             scriptName.remove( 0, 5);
174 
175             // Search script by name
176             ScriptConsoleItem *searchResult = instance()->getScriptListDockWidget()->getScript( scriptName );
177             if (searchResult != nullptr ) {
178                 // Found it - update its console widget
179                 QString logEntry = QString("[%1: %2] %3")
180                 .arg( scriptName )
181                 .arg( context.line )
182                 .arg( msg );
183                 searchResult->appendToConsoleWidget( logEntry );
184             }
185         }
186 
187         // Print all QT logging to STDERR as default
188         std::cerr << msg.toStdString() << std::endl;
189     });
190 }
191 
192 void
slotExecuteNewScript()193 ScriptConsole::slotExecuteNewScript()
194 {
195     if( m_scriptItem->document()->text().isEmpty() )
196         return;
197 
198     m_scriptItem->document()->save();
199     m_scriptItem->start( false );
200 }
201 
202 void
closeEvent(QCloseEvent * event)203 ScriptConsole::closeEvent( QCloseEvent *event )
204 {
205     QSettings settings( QStringLiteral("KDE"), QStringLiteral("Amarok") );
206     settings.beginGroup( QStringLiteral("ScriptConsole") );
207     settings.setValue( QStringLiteral("geometry"), saveGeometry() );
208     settings.setValue( QStringLiteral("savepath"), m_savePath );
209     settings.endGroup();
210     QMainWindow::closeEvent( event );
211     deleteLater();
212 }
213 
214 void
slotEditScript(ScriptConsoleItem * item)215 ScriptConsole::slotEditScript( ScriptConsoleItem *item )
216 {
217     if( m_scriptItem->running() && KMessageBox::warningContinueCancel( this, i18n( "This will stop this script! Continue?" ), QString(), KStandardGuiItem::cont()
218                                         , KStandardGuiItem::cancel(), QStringLiteral("stopRunningScriptWarning") ) == KMessageBox::Cancel )
219         return;
220 
221     item->pause();
222     setCurrentScriptItem( item );
223 }
224 
225 ScriptConsoleItem*
createScriptItem(const QString & script)226 ScriptConsole::createScriptItem( const QString &script )
227 {
228     if( ( m_savePath.isEmpty() || !QDir( m_savePath ).exists() )
229         && ( m_savePath = QFileDialog::getExistingDirectory(this, i18n( "Choose where to save your scripts" ), QStringLiteral("~"),
230             QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks) ).isEmpty() )
231         return 0;
232 
233     QString scriptPath;
234     QString scriptName;
235     do
236     {
237         scriptName = QStringLiteral( "Script-%1" ).arg( qrand() );
238         scriptPath =  QStringLiteral( "%1/%2" ).arg( m_savePath, scriptName );
239     } while ( QDir( scriptPath ).exists() );
240     QDir().mkdir( scriptPath );
241 
242     ScriptEditorDocument *document = new ScriptEditorDocument( this, m_editor->createDocument( 0 ) );
243     document->setText( script );
244     ScriptConsoleItem *scriptItem = new ScriptConsoleItem( this, scriptName, QStringLiteral("Generic"), scriptPath, document );
245     return scriptItem;
246 }
247 
248 ScriptListDockWidget*
getScriptListDockWidget()249 ScriptConsole::getScriptListDockWidget()
250 {
251     return m_scriptListDock;
252 }
253 
~ScriptConsole()254 ScriptConsole::~ScriptConsole()
255 {
256     //m_debugger->detach();
257 }
258 
259 void
slotEvaluationSuspended()260 ScriptConsole::slotEvaluationSuspended()
261 {
262     if( !m_scriptItem )
263     {
264         slotNewScript();
265         return;
266     }
267     debug() << "Is Running() " << m_scriptItem->running();
268     debug() << "Engine isError()" << m_scriptItem->engineResult().isError();
269     // TODO - Inspect if translations work with real debugger signals
270     //if( m_scriptItem->engine() && m_scriptItem->engine()->uncaughtException().isValid() )
271     if( m_scriptItem->engine() && m_scriptItem->engineResult().isError() )
272         return;
273 
274     KTextEditor::View *view = m_scriptItem->getEditorView( m_codeWidget );
275     view->installEventFilter( this );
276     view->document()->installEventFilter( this );
277     m_codeWidget->setWidget( view );
278 }
279 
280 void
slotEvaluationResumed()281 ScriptConsole::slotEvaluationResumed()
282 {
283     debug() << "Is running() " << m_scriptItem->running();
284     debug() << "Engine isError()" << m_scriptItem->engineResult().isError();
285     if( !m_scriptItem->engine() || !m_scriptItem->running() )
286         return;
287 
288     KTextEditor::View *view = m_scriptItem->getEditorView( m_codeWidget );
289     view->installEventFilter( this );
290     m_codeWidget->setWidget( view );
291 }
292 
293 void
slotAbortEvaluation()294 ScriptConsole::slotAbortEvaluation()
295 {
296     m_scriptItem->pause();
297 }
298 
299 
300 QDockWidget*
getWidget(const QString & title,QWidget * widget)301 ScriptConsole::getWidget( const QString &title, QWidget *widget )
302 {
303     QDockWidget *dockWidget = new QDockWidget( title, this );
304     dockWidget->setWidget( widget );
305     return dockWidget;
306 }
307 
308 void
setCurrentScriptItem(ScriptConsoleItem * item)309 ScriptConsole::setCurrentScriptItem( ScriptConsoleItem *item )
310 {
311     if( !item || m_scriptItem.data() == item )
312         return;
313 
314     // Set the active script widgets and update them
315     m_scriptItem = item;
316 
317     KTextEditor::View *view = item->getEditorView( m_codeWidget );
318     m_codeWidget->setWidget( view  );
319     view->installEventFilter( this );
320     view->show();
321 
322     QWidget *console = item->getConsoleWidget( m_consoleWidget );
323     m_consoleWidget->setWidget( console );
324     console->show();
325 
326     QWidget *output = item->getOutputWdiget( m_outputWidget  );
327     m_outputWidget->setWidget( output );
328     output->show();
329 
330     QWidget *error  = item->getErrorWidget( m_errorWidget );
331     m_errorWidget->setWidget( error );
332     error->show();
333 
334     /* TODO - install filters
335     if( item->engine() && item->running() )
336     {
337         view->document()->setReadWrite( false );
338     }
339     else
340     {
341         view->document()->setReadWrite( true );
342         view->installEventFilter( this );
343     }
344     */
345 }
346 
347 void
slotNewScript()348 ScriptConsole::slotNewScript()
349 {
350     ScriptConsoleItem *item = createScriptItem( QLatin1String("") );
351     m_scriptListDock->addScript( item );
352     setCurrentScriptItem( item );
353 }
354 
355 bool
eventFilter(QObject * watched,QEvent * event)356 ScriptConsole::eventFilter( QObject *watched, QEvent *event )
357 {
358     Q_UNUSED( watched )
359     if( event->type() == QEvent::KeyPress )
360     {
361         QKeyEvent *keyEvent = static_cast<QKeyEvent*>( event );
362         if( keyEvent == QKeySequence::MoveToNextPage )
363         {
364             m_scriptListDock->next();
365             return true;
366         }
367         else if( keyEvent == QKeySequence::MoveToPreviousPage )
368         {
369             m_scriptListDock->prev();
370             return true;
371         }
372     }
373     return false;
374 }
375 
ScriptListDockWidget(QWidget * parent)376 ScriptListDockWidget::ScriptListDockWidget( QWidget *parent )
377 : QDockWidget( i18n( "Scripts" ), parent )
378 {
379     QWidget *widget = new BoxWidget( true, this );
380     setWidget( widget );
381     m_scriptListWidget = new QListWidget( widget );
382     m_scriptListWidget->setVerticalScrollMode( QAbstractItemView::ScrollPerPixel );
383     connect( m_scriptListWidget, &QListWidget::doubleClicked,
384              this, &ScriptListDockWidget::slotDoubleClicked );
385     connect( m_scriptListWidget, &QListWidget::currentItemChanged,
386              this, &ScriptListDockWidget::slotCurrentItemChanged );
387 }
388 
389 QListWidget*
listWidget()390 ScriptListDockWidget::listWidget()
391 {
392     return m_scriptListWidget;
393 }
394 
395 void
addScript(ScriptConsoleItem * script)396 ScriptListDockWidget::addScript( ScriptConsoleItem *script )
397 {
398     if( !script )
399         return;
400 
401     QListWidgetItem *item = new QListWidgetItem( script->name(), 0 );
402     item->setData( ScriptRole, QVariant::fromValue<ScriptConsoleItem*>( script ) );
403     m_scriptListWidget->addItem( item );
404     m_scriptListWidget->setCurrentItem( item );
405 }
406 
407 ScriptConsoleItem*
getScript(const QString & scriptName)408 ScriptListDockWidget::getScript( const QString &scriptName)
409 {
410     QList<QListWidgetItem *> searchResult = listWidget()->findItems( scriptName, Qt::MatchFixedString);
411     if (! searchResult.isEmpty() ) {
412         return searchResult.first()->data( ScriptRole ).value<ScriptConsoleItem*>();
413     }
414     return nullptr;
415 }
416 
417 void
removeCurrentScript()418 ScriptListDockWidget::removeCurrentScript()
419 {
420     QListWidgetItem *item = m_scriptListWidget->takeItem( m_scriptListWidget->currentRow() );
421     ScriptConsoleItem *scriptItem = qvariant_cast<ScriptConsoleItem*>( item->data( ScriptRole ) );
422     switch( KMessageBox::warningYesNoCancel( this, i18n( "Remove script file from disk?" ), i18n( "Remove Script" ) ) )
423     {
424         case KMessageBox::Cancel:
425             return;
426         case KMessageBox::Yes:
427             scriptItem->setClearOnDeletion( true );
428         default:
429             break;
430     }
431     scriptItem->stop();
432     scriptItem->deleteLater();
433     delete item;
434 }
435 
436 void
slotCurrentItemChanged(QListWidgetItem * newItem,QListWidgetItem * oldItem)437 ScriptListDockWidget::slotCurrentItemChanged( QListWidgetItem *newItem, QListWidgetItem *oldItem )
438 {
439     Q_UNUSED( oldItem )
440     Q_EMIT currentItemChanged( newItem ? qvariant_cast<ScriptConsoleItem*>( newItem->data(ScriptRole) ) : nullptr );
441 }
442 
443 void
slotDoubleClicked(const QModelIndex & index)444 ScriptListDockWidget::slotDoubleClicked( const QModelIndex &index )
445 {
446     Q_EMIT edit( qvariant_cast<ScriptConsoleItem*>( index.data(ScriptRole) ) );
447 }
448 
449 void
clear()450 ScriptListDockWidget::clear()
451 {
452     if( sender() && KMessageBox::warningContinueCancel( nullptr, i18n("Are you absolutely certain?") ) == KMessageBox::Cancel )
453         return;
454     for( int i = 0; i<m_scriptListWidget->count(); ++i )
455         qvariant_cast<ScriptConsoleItem*>( m_scriptListWidget->item( i )->data( ScriptRole ) )->deleteLater();
456     m_scriptListWidget->clear();
457 
458 }
459 
460 void
addItem(QListWidgetItem * item)461 ScriptListDockWidget::addItem( QListWidgetItem *item )
462 {
463     m_scriptListWidget->addItem( item );
464 }
465 
~ScriptListDockWidget()466 ScriptListDockWidget::~ScriptListDockWidget()
467 {
468     clear();
469 }
470 
471 void
next()472 ScriptListDockWidget::next()
473 {
474     int currentRow = m_scriptListWidget->currentRow();
475     m_scriptListWidget->setCurrentRow( currentRow > 1 ? currentRow - 1 : currentRow );
476 }
477 
478 void
prev()479 ScriptListDockWidget::prev()
480 {
481     int currentRow = m_scriptListWidget->currentRow();
482     m_scriptListWidget->setCurrentRow( currentRow + 1 < m_scriptListWidget->count() ? currentRow + 1 : currentRow );
483 }
484