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