Add support for launching arbitrary executables (tts, manpages etc).

This commit is contained in:
Konstantin Isakov 2011-05-28 22:08:37 -07:00
parent b173898c5d
commit 9960efc00d
17 changed files with 554 additions and 96 deletions

View file

@ -378,6 +378,24 @@ div.sdct_x
color: red;
}
/************* Programs **************/
/* A table which contains a play icon and a word's link */
.programs_play
{
margin-top: 8px;
margin-left: 8px;
}
.programs_play a
{
text-decoration: none;
}
.programs_plaintext, .programs_html
{
margin-top: 15px;
}
/************* MediaWiki articles *****************
The following consist of excerpts from different .css files edited

View file

@ -14,6 +14,7 @@
#include "folding.hh"
#include "wstring_qt.hh"
#include "webmultimediadownload.hh"
#include "programs.hh"
#ifdef Q_OS_WIN32
#include <windows.h>
@ -709,6 +710,35 @@ void ArticleView::openLink( QUrl const & url, QUrl const & ref,
resourceDownloadFinished(); // Check any requests finished already
}
else
if ( url.scheme() == "gdprg" )
{
// Program. Run it.
QString id( url.host() );
for( Config::Programs::const_iterator i = cfg.programs.begin();
i != cfg.programs.end(); ++i )
{
if ( i->id == id )
{
// Found the corresponding program.
Programs::ArticleRequest * req = new Programs::ArticleRequest(
url.path().mid( 1 ), *i );
connect( req, SIGNAL( finished() ), req, SLOT( deleteLater() ) );
// Delete the request if it has finished already
if ( req->isFinished() )
delete req;
return;
}
}
// Still here? No such program exists.
QMessageBox::critical( this, tr( "GoldenDict" ),
tr( "The referenced audio program doesn't exist." ) );
}
else
if ( isExternalLink( url ) )
{
// Use the system handler for the conventional external links
@ -1037,17 +1067,8 @@ void ArticleView::resourceDownloadFinished()
{
ExternalViewer * viewer = new ExternalViewer( this, data, "wav", cfg.preferences.audioPlaybackProgram.trimmed() );
try
{
viewer->start();
// Once started, it will erase itself
}
catch( ... )
{
delete viewer;
throw;
}
// Once started, it will erase itself
viewer->start();
}
catch( ExternalViewer::Ex & e )
{
@ -1091,7 +1112,7 @@ void ArticleView::resourceDownloadFinished()
}
else
{
// This one had no data. Erase it.
// This one had no data. Erase it.
resourceDownloadRequests.erase( i++ );
}
}

View file

@ -162,6 +162,20 @@ WebSites makeDefaultWebSites()
return ws;
}
Programs makeDefaultPrograms()
{
Programs programs;
// The following list doesn't make a lot of sense under Windows
#ifndef Q_WS_WIN
programs.push_back( Program( false, Program::Audio, "428b4c2b905ef568a43d9a16f59559b0", "Festival", "festival --tts" ) );
programs.push_back( Program( false, Program::Audio, "2cf8b3a60f27e1ac812de0b57c148340", "Espeak", "espeak %GDWORD%" ) );
programs.push_back( Program( false, Program::PlainText, "4f898f7582596cea518c6b0bfdceb8b3", "Manpages", "man -a %GDWORD%" ) );
#endif
return programs;
}
/// Sets option to true of false if node is "1" or "0" respectively, or leaves
/// it intact if it's neither "1" nor "0".
void applyBoolOption( bool & option, QDomNode const & node )
@ -459,6 +473,30 @@ Class load() throw( exError )
else
c.forvo.languageCodes = "en, ru"; // Default demo values
QDomNode programs = root.namedItem( "programs" );
if ( !programs.isNull() )
{
QDomNodeList nl = programs.toElement().elementsByTagName( "program" );
for( unsigned x = 0; x < nl.length(); ++x )
{
QDomElement pr = nl.item( x ).toElement();
Program p;
p.id = pr.attribute( "id" );
p.name = pr.attribute( "name" );
p.commandLine = pr.attribute( "commandLine" );
p.enabled = ( pr.attribute( "enabled" ) == "1" );
p.type = (Program::Type)( pr.attribute( "type" ).toInt() );
c.programs.push_back( p );
}
}
else
c.programs = makeDefaultPrograms();
QDomNode mws = root.namedItem( "mediawikis" );
if ( !mws.isNull() )
@ -916,6 +954,37 @@ void save( Class const & c ) throw( exError )
}
}
{
QDomElement programs = dd.createElement( "programs" );
root.appendChild( programs );
for( Programs::const_iterator i = c.programs.begin(); i != c.programs.end(); ++i )
{
QDomElement p = dd.createElement( "program" );
programs.appendChild( p );
QDomAttr id = dd.createAttribute( "id" );
id.setValue( i->id );
p.setAttributeNode( id );
QDomAttr name = dd.createAttribute( "name" );
name.setValue( i->name );
p.setAttributeNode( name );
QDomAttr commandLine = dd.createAttribute( "commandLine" );
commandLine.setValue( i->commandLine );
p.setAttributeNode( commandLine );
QDomAttr enabled = dd.createAttribute( "enabled" );
enabled.setValue( i->enabled ? "1" : "0" );
p.setAttributeNode( enabled );
QDomAttr type = dd.createAttribute( "type" );
type.setValue( QString::number( i->type ) );
p.setAttributeNode( type );
}
}
{
QDomElement muted = dd.createElement( "mutedDictionaries" );

View file

@ -304,6 +304,39 @@ struct Forvo
{ return ! operator == ( other ); }
};
struct Program
{
bool enabled;
enum Type
{
Audio,
PlainText,
Html,
MaxTypeValue
} type;
QString id, name, commandLine;
Program(): enabled( false )
{}
Program( bool enabled_, Type type_, QString const & id_,
QString const & name_, QString const & commandLine_ ):
enabled( enabled_ ), type( type_ ), id( id_ ), name( name_ ),
commandLine( commandLine_ ) {}
bool operator == ( Program const & other ) const
{ return enabled == other.enabled &&
type == other.type &&
name == other.name &&
commandLine == other.commandLine;
}
bool operator != ( Program const & other ) const
{ return ! operator == ( other ); }
};
typedef vector< Program > Programs;
/// Dictionaries which are temporarily disabled via the dictionary bar.
typedef QSet< QString > MutedDictionaries;
@ -320,6 +353,7 @@ struct Class
Hunspell hunspell;
Transliteration transliteration;
Forvo forvo;
Programs programs;
unsigned lastMainGroupId; // Last used group in main window
unsigned lastPopupGroupId; // Last used group in popup window

View file

@ -15,6 +15,8 @@
#include "config.hh"
#include <QDir>
#include <QFileInfo>
#include <QCryptographicHash>
#include <QDateTime>
namespace Dictionary {
@ -218,4 +220,13 @@ bool needToRebuildIndex( vector< string > const & dictionaryFiles,
return fileInfo.lastModified().toTime_t() < lastModified;
}
QString generateRandomDictionaryId()
{
return QString(
QCryptographicHash::hash(
QDateTime::currentDateTime().toString( "\"Random\"dd.MM.yyyy hh:mm:ss.zzz" ).toUtf8(),
QCryptographicHash::Md5 ).toHex() );
}
}

View file

@ -403,6 +403,10 @@ string makeDictionaryId( vector< string > const & dictionaryFiles ) throw();
bool needToRebuildIndex( vector< string > const & dictionaryFiles,
string const & indexFile ) throw();
/// Returns a random dictionary id useful for interactively created
/// dictionaries.
QString generateRandomDictionaryId();
}
#endif

View file

@ -16,7 +16,7 @@ EditDictionaries::EditDictionaries( QWidget * parent, Config::Class & cfg_,
dictNetMgr( dictNetMgr_ ),
origCfg( cfg ),
sources( this, cfg.paths, cfg.soundDirs, cfg.hunspell, cfg.transliteration,
cfg.forvo, cfg.mediawikis, cfg.webSites ),
cfg.forvo, cfg.mediawikis, cfg.webSites, cfg.programs ),
orderAndProps( new OrderAndProps( this, cfg.dictionaryOrder, cfg.inactiveDictionaries,
dictionaries ) ),
groups( new Groups( this, dictionaries, cfg.groups, orderAndProps->getCurrentDictionaryOrder() ) ),
@ -140,7 +140,8 @@ bool EditDictionaries::isSourcesChanged() const
sources.getTransliteration() != cfg.transliteration ||
sources.getForvo() != cfg.forvo ||
sources.getMediaWikis() != cfg.mediawikis ||
sources.getWebSites() != cfg.webSites;
sources.getWebSites() != cfg.webSites ||
sources.getPrograms() != cfg.programs;
}
void EditDictionaries::acceptChangedSources( bool rebuildGroups )
@ -158,6 +159,7 @@ void EditDictionaries::acceptChangedSources( bool rebuildGroups )
cfg.forvo = sources.getForvo();
cfg.mediawikis = sources.getMediaWikis();
cfg.webSites = sources.getWebSites();
cfg.programs = sources.getPrograms();
groupInstances.clear(); // Those hold pointers to dictionaries, we need to
// free them.

View file

@ -44,7 +44,7 @@ private slots:
void on_tabs_currentChanged( int index );
void rescanSources();
private:
bool isSourcesChanged() const;

View file

@ -1,8 +1,8 @@
/* This file is (c) 2008-2011 Konstantin Isakov <ikm@goldendict.org>
* Part of GoldenDict. Licensed under GPLv3 or later, see the LICENSE file */
#include "externalviewer.hh"
#include <QDir>
#include "externalviewer.hh"
using std::vector;
@ -23,47 +23,17 @@ ExternalViewer::ExternalViewer( QObject * parent, vector< char > const & data,
tempFile.close();
printf( "%s\n", tempFile.fileName().toLocal8Bit().data() );
connect( &viewer, SIGNAL( finished( int, QProcess::ExitStatus ) ),
this, SLOT( viewerFinished( int, QProcess::ExitStatus ) ) );
connect( this, SIGNAL( finished( ExternalViewer * ) ),
&ExternalViewerDeleter::instance(), SLOT( deleteExternalViewer( ExternalViewer * ) ),
Qt::QueuedConnection );
}
ExternalViewer::~ExternalViewer()
{
// No need to delete us once we're being destructed. This fixes some
// double-free corruption if the object is being freed prematurely.
disconnect( this, SIGNAL( finished( ExternalViewer * ) ),
&ExternalViewerDeleter::instance(), SLOT( deleteExternalViewer( ExternalViewer * ) ) );
}
void ExternalViewer::start() throw( exCantRunViewer )
{
connect( &viewer, SIGNAL( finished( int, QProcess::ExitStatus ) ),
this, SLOT( deleteLater() ) );
connect( &viewer, SIGNAL( error( QProcess::ProcessError ) ),
this, SLOT( deleteLater() ) );
viewer.start( viewerProgram, QStringList( tempFileName ), QIODevice::NotOpen );
if ( !viewer.waitForStarted() )
throw exCantRunViewer( viewerProgram.toStdString() );
}
void ExternalViewer::viewerFinished( int, QProcess::ExitStatus )
{
emit finished( this );
}
ExternalViewerDeleter & ExternalViewerDeleter::instance()
{
static ExternalViewerDeleter evd( 0 );
return evd;
}
void ExternalViewerDeleter::deleteExternalViewer( ExternalViewer * e )
{
printf( "Deleting external viewer\n" );
delete e;
}

View file

@ -30,35 +30,9 @@ public:
QString const & extension, QString const & viewerProgram )
throw( exCantCreateTempFile );
~ExternalViewer();
// Once this is called, the object will be deleted when it's done, even if
// the function throws.
void start() throw( exCantRunViewer );
private slots:
void viewerFinished( int, QProcess::ExitStatus );
signals:
void finished( ExternalViewer * );
};
class ExternalViewerDeleter: public QObject
{
Q_OBJECT
public:
static ExternalViewerDeleter & instance();
public slots:
void deleteExternalViewer( ExternalViewer * e );
private:
ExternalViewerDeleter( QObject * parent ): QObject( parent )
{}
};
#endif

View file

@ -156,7 +156,8 @@ HEADERS += folding.hh \
webmultimediadownload.hh \
forvo.hh \
country.hh \
about.hh
about.hh \
programs.hh
FORMS += groups.ui \
dictgroupwidget.ui \
mainwindow.ui \
@ -238,7 +239,8 @@ SOURCES += folding.cc \
webmultimediadownload.cc \
forvo.cc \
country.cc \
about.cc
about.cc \
programs.cc
win32 {
SOURCES += mouseover_win32/ThTypes.c
HEADERS += mouseover_win32/ThTypes.h

BIN
icons/programs.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View file

@ -17,6 +17,7 @@
#include "greektranslit.hh"
#include "website.hh"
#include "forvo.hh"
#include "programs.hh"
#include <QMessageBox>
#include <QDir>
@ -230,6 +231,14 @@ void loadDictionaries( QWidget * parent, bool showInitially,
dictionaries.insert( dictionaries.end(), dicts.begin(), dicts.end() );
}
//// Programs
{
vector< sptr< Dictionary::Class > > dicts =
Programs::makeDictionaries( cfg.programs );
dictionaries.insert( dictionaries.end(), dicts.begin(), dicts.end() );
}
printf( "Load done\n" );
// Remove any stale index files

View file

@ -13,6 +13,7 @@
<file>icons/reload.png</file>
<file>icons/programicon.png</file>
<file>icons/programicon_scan.png</file>
<file>icons/programs.png</file>
<file>icons/wizard.png</file>
<file>icons/warning.png</file>
<file>article-style.css</file>

View file

@ -4,8 +4,6 @@
#include "sources.hh"
#include <QFileDialog>
#include <QMessageBox>
#include <QCryptographicHash>
#include <QDateTime>
#include <QStandardItemModel>
Sources::Sources( QWidget * parent, Config::Paths const & paths,
@ -14,15 +12,28 @@ Sources::Sources( QWidget * parent, Config::Paths const & paths,
Config::Transliteration const & trs,
Config::Forvo const & forvo,
Config::MediaWikis const & mediawikis,
Config::WebSites const & webSites ): QWidget( parent ),
Config::WebSites const & webSites,
Config::Programs const & programs ): QWidget( parent ),
itemDelegate( new QItemDelegate( this ) ),
itemEditorFactory( new QItemEditorFactory() ),
mediawikisModel( this, mediawikis ),
webSitesModel( this, webSites ),
programsModel( this, programs ),
pathsModel( this, paths ),
soundDirsModel( this, soundDirs ),
hunspellDictsModel( this, hunspell )
{
ui.setupUi( this );
// TODO: will programTypeEditorCreator and itemEditorFactory be destoryed by
// anyone?
QItemEditorCreatorBase * programTypeEditorCreator =
new QStandardItemEditorCreator< ProgramTypeEditor >();
itemEditorFactory->registerEditor( QVariant::Int, programTypeEditorCreator );
itemDelegate->setItemEditorFactory( itemEditorFactory );
ui.mediaWikis->setTabKeyNavigation( true );
ui.mediaWikis->setModel( &mediawikisModel );
ui.mediaWikis->resizeColumnToContents( 0 );
@ -35,6 +46,17 @@ Sources::Sources( QWidget * parent, Config::Paths const & paths,
ui.webSites->resizeColumnToContents( 1 );
ui.webSites->resizeColumnToContents( 2 );
ui.programs->setTabKeyNavigation( true );
ui.programs->setModel( &programsModel );
ui.programs->resizeColumnToContents( 0 );
// Make sure this thing will be large enough
ui.programs->setColumnWidth( 1,
QFontMetrics( QFont() ).width(
ProgramTypeEditor::getNameForType( Config::Program::PlainText ) ) + 16 );
ui.programs->resizeColumnToContents( 2 );
ui.programs->resizeColumnToContents( 3 );
ui.programs->setItemDelegate( itemDelegate );
ui.paths->setTabKeyNavigation( true );
ui.paths->setModel( &pathsModel );
@ -226,6 +248,30 @@ void Sources::on_removeWebSite_clicked()
webSitesModel.removeSite( current.row() );
}
void Sources::on_addProgram_clicked()
{
programsModel.addNewProgram();
QModelIndex result =
programsModel.index( programsModel.rowCount( QModelIndex() ) - 1,
1, QModelIndex() );
ui.programs->scrollTo( result );
ui.programs->edit( result );
}
void Sources::on_removeProgram_clicked()
{
QModelIndex current = ui.programs->currentIndex();
if ( current.isValid() &&
QMessageBox::question( this, tr( "Confirm removal" ),
tr( "Remove program <b>%1</b> from the list?" ).arg( programsModel.getCurrentPrograms()[ current.row() ].name ),
QMessageBox::Ok,
QMessageBox::Cancel ) == QMessageBox::Ok )
programsModel.removeProgram( current.row() );
}
Config::Hunspell Sources::getHunspell() const
{
Config::Hunspell h;
@ -284,11 +330,7 @@ void MediaWikisModel::addNewWiki()
w.enabled = false;
// That's quite some rng
w.id = QString(
QCryptographicHash::hash(
QDateTime::currentDateTime().toString( "\"MediaWiki\"dd.MM.yyyy hh:mm:ss.zzz" ).toUtf8(),
QCryptographicHash::Md5 ).toHex() );
w.id = Dictionary::generateRandomDictionaryId();
w.url = "http://";
@ -437,11 +479,7 @@ void WebSitesModel::addNewSite()
w.enabled = false;
// That's quite some rng
w.id = QString(
QCryptographicHash::hash(
QDateTime::currentDateTime().toString( "\"WebSite\"dd.MM.yyyy hh:mm:ss.zzz" ).toUtf8(),
QCryptographicHash::Md5 ).toHex() );
w.id = Dictionary::generateRandomDictionaryId();
w.url = "http://";
@ -570,6 +608,191 @@ bool WebSitesModel::setData( QModelIndex const & index, const QVariant & value,
}
////////// ProgramsModel
ProgramsModel::ProgramsModel( QWidget * parent,
Config::Programs const & programs_ ):
QAbstractItemModel( parent ), programs( programs_ )
{
}
void ProgramsModel::removeProgram( int index )
{
beginRemoveRows( QModelIndex(), index, index );
programs.erase( programs.begin() + index );
endRemoveRows();
}
void ProgramsModel::addNewProgram()
{
Config::Program p;
p.enabled = false;
p.id = Dictionary::generateRandomDictionaryId();
beginInsertRows( QModelIndex(), programs.size(), programs.size() );
programs.push_back( p );
endInsertRows();
}
QModelIndex ProgramsModel::index( int row, int column, QModelIndex const & /*parent*/ ) const
{
return createIndex( row, column, 0 );
}
QModelIndex ProgramsModel::parent( QModelIndex const & /*parent*/ ) const
{
return QModelIndex();
}
Qt::ItemFlags ProgramsModel::flags( QModelIndex const & index ) const
{
Qt::ItemFlags result = QAbstractItemModel::flags( index );
if ( index.isValid() )
{
if ( !index.column() )
result |= Qt::ItemIsUserCheckable;
else
result |= Qt::ItemIsEditable;
}
return result;
}
int ProgramsModel::rowCount( QModelIndex const & parent ) const
{
if ( parent.isValid() )
return 0;
else
return programs.size();
}
int ProgramsModel::columnCount( QModelIndex const & parent ) const
{
if ( parent.isValid() )
return 0;
else
return 4;
}
QVariant ProgramsModel::headerData( int section, Qt::Orientation /*orientation*/, int role ) const
{
if ( role == Qt::DisplayRole )
switch( section )
{
case 0:
return tr( "Enabled" );
case 1:
return tr( "Type" );
case 2:
return tr( "Name" );
case 3:
return tr( "Command Line" );
default:
return QVariant();
}
return QVariant();
}
QVariant ProgramsModel::data( QModelIndex const & index, int role ) const
{
if ( (unsigned) index.row() >= programs.size() )
return QVariant();
if ( role == Qt::DisplayRole || role == Qt::EditRole )
{
switch( index.column() )
{
case 1:
if ( role == Qt::DisplayRole )
return ProgramTypeEditor::getNameForType( programs[ index.row() ].type );
else
return QVariant( ( int ) programs[ index.row() ].type );
case 2:
return programs[ index.row() ].name;
case 3:
return programs[ index.row() ].commandLine;
default:
return QVariant();
}
}
if ( role == Qt::CheckStateRole && !index.column() )
return programs[ index.row() ].enabled;
return QVariant();
}
bool ProgramsModel::setData( QModelIndex const & index, const QVariant & value,
int role )
{
if ( (unsigned)index.row() >= programs.size() )
return false;
if ( role == Qt::CheckStateRole && !index.column() )
{
programs[ index.row() ].enabled = !programs[ index.row() ].enabled;
dataChanged( index, index );
return true;
}
if ( role == Qt::DisplayRole || role == Qt::EditRole )
switch( index.column() )
{
case 1:
programs[ index.row() ].type = Config::Program::Type( value.toInt() );
dataChanged( index, index );
return true;
case 2:
programs[ index.row() ].name = value.toString();
dataChanged( index, index );
return true;
case 3:
programs[ index.row() ].commandLine = value.toString();
dataChanged( index, index );
return true;
default:
return false;
}
return false;
}
QString ProgramTypeEditor::getNameForType( int v )
{
switch( v )
{
case Config::Program::Audio:
return tr( "Audio" );
case Config::Program::PlainText:
return tr( "Plain Text" );
case Config::Program::Html:
return tr( "Html" );
default:
return tr( "Unknown" );
}
}
ProgramTypeEditor::ProgramTypeEditor( QWidget * widget ): QComboBox( widget )
{
for( int x = 0; x < Config::Program::MaxTypeValue; ++x )
addItem( getNameForType( x ) );
}
int ProgramTypeEditor::getType() const
{
return currentIndex();
}
void ProgramTypeEditor::setType( int t )
{
setCurrentIndex( t );
}
////////// PathsModel
PathsModel::PathsModel( QWidget * parent,

View file

@ -8,6 +8,9 @@
#include "config.hh"
#include "hunspell.hh"
#include <QAbstractItemModel>
#include <QComboBox>
#include <QItemDelegate>
#include <QItemEditorFactory>
/// A model to be projected into the mediawikis view, according to Qt's MVC model
class MediaWikisModel: public QAbstractItemModel
@ -69,6 +72,52 @@ private:
Config::WebSites webSites;
};
/// A model to be projected into the programs view, according to Qt's MVC model
class ProgramsModel: public QAbstractItemModel
{
Q_OBJECT
public:
ProgramsModel( QWidget * parent, Config::Programs const & );
void removeProgram( int index );
void addNewProgram();
/// Returns the sites the model currently has listed
Config::Programs const & getCurrentPrograms() const
{ return programs; }
QModelIndex index( int row, int column, QModelIndex const & parent ) const;
QModelIndex parent( QModelIndex const & parent ) const;
Qt::ItemFlags flags( QModelIndex const & index ) const;
int rowCount( QModelIndex const & parent ) const;
int columnCount( QModelIndex const & parent ) const;
QVariant headerData( int section, Qt::Orientation orientation, int role ) const;
QVariant data( QModelIndex const & index, int role ) const;
bool setData( QModelIndex const & index, const QVariant & value, int role );
private:
Config::Programs programs;
};
class ProgramTypeEditor: public QComboBox
{
Q_OBJECT
Q_PROPERTY(int type READ getType WRITE setType USER true)
public:
ProgramTypeEditor( QWidget * widget = 0 );
// Returns localized name for the given program type
static QString getNameForType( int );
public:
int getType() const;
void setType( int );
};
/// A model to be projected into the paths view, according to Qt's MVC model
class PathsModel: public QAbstractItemModel
{
@ -171,7 +220,8 @@ public:
Config::Transliteration const &,
Config::Forvo const & forvo,
Config::MediaWikis const &,
Config::WebSites const & );
Config::WebSites const &,
Config::Programs const &);
Config::Paths const & getPaths() const
{ return pathsModel.getCurrentPaths(); }
@ -185,6 +235,9 @@ public:
Config::WebSites const & getWebSites() const
{ return webSitesModel.getCurrentWebSites(); }
Config::Programs const & getPrograms() const
{ return programsModel.getCurrentPrograms(); }
Config::Hunspell getHunspell() const;
Config::Transliteration getTransliteration() const;
@ -198,8 +251,13 @@ signals:
private:
Ui::Sources ui;
QItemDelegate * itemDelegate;
QItemEditorFactory * itemEditorFactory;
MediaWikisModel mediawikisModel;
WebSitesModel webSitesModel;
ProgramsModel programsModel;
PathsModel pathsModel;
SoundDirsModel soundDirsModel;
HunspellDictsModel hunspellDictsModel;
@ -224,6 +282,9 @@ private slots:
void on_addWebSite_clicked();
void on_removeWebSite_clicked();
void on_addProgram_clicked();
void on_removeProgram_clicked();
void on_rescan_clicked();
};

View file

@ -6,7 +6,7 @@
<rect>
<x>0</x>
<y>0</y>
<width>665</width>
<width>690</width>
<height>336</height>
</rect>
</property>
@ -334,6 +334,65 @@ of the appropriate groups to use them.</string>
</item>
</layout>
</widget>
<widget class="QWidget" name="tab_6">
<attribute name="icon">
<iconset resource="resources.qrc">
<normaloff>:/icons/programs.png</normaloff>:/icons/programs.png</iconset>
</attribute>
<attribute name="title">
<string>Programs</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_15">
<item>
<widget class="QLabel" name="label_16">
<property name="text">
<string>Any external programs. A string %GDWORD% will be replaced with the query word. The word will also be fed into standard input.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_7">
<item>
<widget class="QTreeView" name="programs"/>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout_14">
<item>
<widget class="QPushButton" name="addProgram">
<property name="text">
<string>&amp;Add...</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="removeProgram">
<property name="text">
<string>&amp;Remove</string>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer_12">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</item>
</layout>
</widget>
<widget class="QWidget" name="tab_5">
<attribute name="icon">
<iconset resource="resources.qrc">