Compare commits

..

51 Commits

Author SHA1 Message Date
8a47838d94 EntryProvider: Support broken .desktop files that don't start with [Desktop Entry] 2022-07-15 18:26:02 +02:00
89c9cc573e Add more reasonable screenshots 2021-10-30 16:22:43 +02:00
7c15d7b145 Window: executeConfig: Fix after refactoring for Qt 5.15
Refactoring for 5.15 broke this (d9b0be5063),
so fix it.
2021-05-08 20:54:19 +02:00
d9b0be5063 Fix some Qt deprication warnings 2021-04-29 09:33:57 +02:00
f1b17c0842 Ranking: Prioritizee name before command/binary name
This seems to work better for flatpak apps and should
argueably be more natural.
2021-04-29 09:33:31 +02:00
4315de0e76 EntryPushButton: Hardcode max 256x256 icon size for now
While dirty, in practise, this appears to be working more fine than selecting
the max size. Some of those were too big.

Relevant: #32.
2021-03-19 12:00:40 +01:00
f0387db468 Add a basic ranking of system entries and show most relevant first 2021-03-16 22:29:26 +01:00
6e4f0ecabf Window: filterGridFor: Search button Command too 2021-03-05 20:51:14 +01:00
2df37212dd EntryPushButton: Don't allow moving dynamic buttons 2020-10-19 21:45:33 +02:00
b13815a924 executeConfig: Open terminal if Shift pressed 2020-10-19 21:45:33 +02:00
bb74d156c9 Introduce new button type 'DYNAMIC' for PATH suggestions
Fixes #23
2020-10-19 21:45:33 +02:00
f9038f3098 EntryProvider: Don't use 'new' when throwing exceptions 2020-10-19 21:45:18 +02:00
042b53a0be Window: addToFavourites(): Discard shortcut from system entries 2020-10-19 21:36:59 +02:00
64d3223fb2 EntryProvider: saveUserEntry: Only save key field if it's not empty 2020-10-19 21:30:11 +02:00
adfb065358 Window: keyPressEvent: don't show empty shortcuts 2020-10-10 22:25:40 +02:00
98cff50c41 main: don't ignore config value for single instance mode. allow override using cli param 2020-10-10 21:58:37 +02:00
5b5333585c SettingsProvider/main: Read single instance socket path from config 2020-10-10 21:58:37 +02:00
8eb52fbfb3 main: allow overriding config dir from commandline 2020-10-10 21:58:37 +02:00
241bec54ad Parse argv using QCommandLineParser
Fixes: #17
2020-10-10 21:58:37 +02:00
ac5498990b EntryProvider: Introduce isSavable() to avoid code duplicates 2020-10-08 22:50:38 +02:00
38cc87fbcd add 'terminal=true' support for .qsrun entries 2020-10-08 22:27:48 +02:00
2d14b01c5c EntryPushButton: Set term icon for terminal cmds w/o icon 2020-10-08 22:27:48 +02:00
59ff382856 Implement basic 'Terminal=' support for .desktop files 2020-10-04 22:19:30 +02:00
bf8f09ec66 Delete TODO
Issue and TODO Tracker resides here:
https://gitea.quitesimple.org/crtxcr/qsrun/issues
2020-10-04 21:57:15 +02:00
776cbe4d30 EntryProvider: saveUserEntry(): Add missing fields 2020-10-04 21:48:35 +02:00
101885ffe4 window: addToFavourites(): Save non-system entries in their originating path 2020-10-04 21:48:35 +02:00
79d15fb628 Introduce EntryType. Retire userEntry boolean
Introduce EntryType, allowing to distnguish between:

- inherietd entries: Those inherit their values from
other entries, typcally system entries. They do not
allow overwriting the value, except row/col.
- system entries: those not created by the user in any way,
usually .desktop files in /usr/share/applications/...
- user entries: those that have been created by the user.
they can however also inherit, and overwrite inherited values.

inherited are used for the "favourites" feature.
2020-10-04 21:48:35 +02:00
95e855f325 EntryProvider: readqsrunfile(): impl. inherit w/o using EntityConfig.update() 2020-10-04 21:48:35 +02:00
2acfe6126a EntryProvider: readqsrunfile(): Simplify by using QHash 2020-10-04 21:48:35 +02:00
2d79d74e06 addToFavourites: only save the minimum 2020-10-04 21:48:35 +02:00
7c2461626d rename Window::buttonClick to Window::executeConfig 2020-10-04 21:48:35 +02:00
ae7f310ab2 window: use EntryConfig, not EntryPushButton for slots
No point to implicitly convert them, in fact it was a mistake.
2020-10-04 21:48:35 +02:00
2ab0750f3a EntryConfig: update(): Add missing assignments 2020-10-04 21:48:35 +02:00
f9eff3b5b2 implement deletion of entries 2020-10-04 21:48:35 +02:00
84cf8837bd addToFavourites: catch exception when saving entry 2020-10-04 21:48:35 +02:00
47c0358a25 window: getNextFreeCell(): reimplement without refering to grid
Does not work that way since when we search, the grid has changed.
So we cannot check inside the grid which ones are free. This
would give wrong results.
2020-10-04 21:48:35 +02:00
7bae1183c6 EnrtyConfig: store iconPath, not QIcon. Keeps icon after rearranging entries 2020-10-04 21:48:35 +02:00
9daaba2543 Window: buttonClick(): Correct parameter name 2020-10-04 21:48:35 +02:00
6b924c0f53 Implement addToFavourites() and getNextFreeCell() 2020-10-04 21:48:35 +02:00
653089c475 Save rearranged EntryConfig of buttons with new EntryProvider::saveUserEntry 2020-10-04 21:48:35 +02:00
f06e9a4f7b EntryConfig: Also store 'inherit' key. Needed to save entry later. 2020-09-28 19:07:26 +02:00
61001ed6cc EntryProvider: Set entryPath also for system entries 2020-09-28 19:07:26 +02:00
e2c80b665e EntryPushButton: Begin menus for deletion/favourites 2020-09-28 19:07:26 +02:00
2a9292958b EntryPushButton: Enter drag mode only for user entries
Maybe the decision should not be up to the button, but
for now this will do.
2020-09-28 19:07:26 +02:00
27ac3155d3 EntryConfig: Introduce (dirty) way to distinguish between user/system entries 2020-09-28 19:07:26 +02:00
b72931cc9e GUI: Begin basic drag/drop between buttons: Allow swapping places
Issue: #7
2020-09-28 19:07:26 +02:00
b588fd07be EntryProvider: fix handling of names with spaces 2020-09-28 19:07:05 +02:00
5eb09540c6 window: get number of columns from config 2020-09-07 23:29:10 +02:00
416bfa6314 MINOR: window.cpp: Reformat code
No functional change.
2020-09-07 23:16:45 +02:00
edb781580e EntryProvider: Introduce readEntryFromPath()
Inheritance: Use readEntryFromPath instead of assuming always .desktop
files

Switch project to C++17
2020-09-06 22:24:39 +02:00
36b6390292 Implement NoDisplay= of .desktop entries
Closes #9
2020-09-06 22:09:02 +02:00
15 changed files with 689 additions and 204 deletions

4
TODO
View File

@ -1,4 +0,0 @@
- Investigate memory leaks (relevant now that we have a single user mode,
so qsrun may stay open after launch)
- Provide --help
- ESC/CTRL+Q close the app. May not be expected behaviour

View File

@ -13,6 +13,11 @@ EntryProvider::EntryProvider(QStringList userEntriesDirsPaths, QStringList syste
<< "%u";
}
bool EntryProvider::isSavable(const EntryConfig &config) const
{
return ! config.entryPath.isEmpty() && (config.type == EntryType::USER || config.type == EntryType::INHERIT);
}
EntryConfig EntryProvider::readFromDesktopFile(const QString &path)
{
EntryConfig result;
@ -20,20 +25,22 @@ EntryConfig EntryProvider::readFromDesktopFile(const QString &path)
if(!file.open(QIODevice::ReadOnly | QIODevice::Text))
{
// TODO: better exception class
throw new std::runtime_error("Failed to open file");
throw std::runtime_error("Failed to open file");
}
QTextStream stream(&file);
// There should be nothing preceding this group in the desktop entry file but possibly one or more comments.
// https://standards.freedesktop.org/desktop-entry-spec/latest/ar01s03.html#group-header
QString firstLine;
// Ignore that as there some that violate that in the wild
const QString startSection = "[Desktop Entry]";
QString line;
do
{
firstLine = stream.readLine().trimmed();
} while(!stream.atEnd() && (firstLine.isEmpty() || firstLine[0] == '#'));
line = stream.readLine().trimmed();
} while(!stream.atEnd() && line != startSection);
if(firstLine != "[Desktop Entry]")
if(line != startSection)
{
throw ConfigFormatException(".desktop file does not start with [Desktop Entry]: " + path.toStdString());
throw ConfigFormatException(".desktop file does not contain [Desktop Entry] section: " + path.toStdString());
}
while(!stream.atEnd())
@ -56,7 +63,7 @@ EntryConfig EntryProvider::readFromDesktopFile(const QString &path)
}
if(key == "icon")
{
result.icon = QIcon::fromTheme(args);
result.iconPath = args;
}
if(key == "exec")
{
@ -75,12 +82,39 @@ EntryConfig EntryProvider::readFromDesktopFile(const QString &path)
}
}
}
if(key == "nodisplay")
{
result.hidden = args == "true";
}
if(key == "terminal")
{
result.isTerminalCommand = args == "true";
}
}
result.type = EntryType::SYSTEM;
return result;
}
/* qsrun own's config file */
EntryConfig EntryProvider::readFromFile(const QString &path)
std::optional<EntryConfig> EntryProvider::readEntryFromPath(const QString &path)
{
QFileInfo info(path);
if(info.isFile())
{
QString suffix = info.suffix();
if(suffix == "desktop")
{
return readFromDesktopFile(path);
}
if(suffix == "qsrun")
{
return readqsrunFile(path);
}
}
return {};
}
/* qsrun's own format */
EntryConfig EntryProvider::readqsrunFile(const QString &path)
{
EntryConfig result;
EntryConfig inheritedConfig;
@ -88,21 +122,73 @@ EntryConfig EntryProvider::readFromFile(const QString &path)
if(!file.open(QIODevice::ReadOnly | QIODevice::Text))
{
// TODO: better exception class
throw new std::runtime_error("Failed to open file");
throw std::runtime_error("Failed to open file");
}
QHash<QString, QString> map;
QTextStream stream(&file);
while(!stream.atEnd())
{
QString line = stream.readLine();
QStringList splitted = line.split(" ");
if(splitted.length() < 2)
int spacePos = line.indexOf(' ');
if(spacePos == -1)
{
throw new ConfigFormatException("misformated line in .qsrun config file " + path.toStdString());
throw ConfigFormatException("misformated line in .qsrun config file " + path.toStdString());
}
QString key = splitted[0];
if(key == "arguments")
QString key = line.mid(0, spacePos);
QString value = line.mid(spacePos + 1);
if(key == "" || value == "")
{
auto args = splitted.mid(1);
throw ConfigFormatException("empty key or value in .qsrun config file " + path.toStdString());
}
map[key] = value;
}
if(map.contains("inherit"))
{
auto entry = readEntryFromPath(map["inherit"]);
if(entry)
{
result = entry.value();
result.inherit = map["inherit"];
}
else
{
throw ConfigFormatException("Error attempting to read inherited entry");
}
}
QString type = map["type"];
if(!type.isEmpty())
{
if(type == "system")
{
throw ConfigFormatException(".qsrun files cannot be designated as system entries " +
path.toStdString());
}
else if(type == "inherit")
{
result.type = EntryType::INHERIT;
}
else if(type == "user")
{
result.type = EntryType::USER;
}
else
{
throw ConfigFormatException("Invalid value for type provided in file: " + path.toStdString());
}
}
else
{
result.type = EntryType::USER;
}
if(result.type != EntryType::INHERIT)
{
if(map.contains("arguments"))
{
auto args = map["arguments"].split(' ');
QString merged;
for(QString &str : args)
{
@ -131,38 +217,21 @@ EntryConfig EntryProvider::readFromFile(const QString &path)
throw ConfigFormatException("non-closed \" in config file " + path.toStdString());
}
}
if(key == "name")
auto assignIfSourceNotEmpty = [](QString source, QString &val) {
if(!source.isEmpty())
{
result.name = splitted[1];
val = source;
}
if(key == "icon")
{
result.icon = QIcon(splitted[1]);
};
assignIfSourceNotEmpty(map["key"].toLower(), result.key);
assignIfSourceNotEmpty(map["command"], result.command);
assignIfSourceNotEmpty(map["icon"], result.iconPath);
assignIfSourceNotEmpty(map["name"], result.name);
}
if(key == "row")
{
result.row = splitted[1].toInt();
}
if(key == "col")
{
result.col = splitted[1].toInt();
}
if(key == "command")
{
result.command = splitted[1];
}
if(key == "key")
{
// QKeySequence sequence(splitted[1]);
// result.keySequence = sequence;
result.key = splitted[1].toLower();
}
if(key == "inherit")
{
inheritedConfig = readFromDesktopFile(resolveEntryPath(splitted[1]));
}
}
return result.update(inheritedConfig);
result.col = map["col"].toInt();
result.row = map["row"].toInt();
result.isTerminalCommand = map["terminal"] == "true";
return result;
}
QString EntryProvider::resolveEntryPath(QString path)
@ -197,17 +266,13 @@ QVector<EntryConfig> EntryProvider::readConfig(QStringList paths)
while(it.hasNext())
{
QString path = it.next();
QFileInfo info(path);
if(info.isFile())
std::optional<EntryConfig> entry = readEntryFromPath(path);
if(entry)
{
QString suffix = info.suffix();
if(suffix == "desktop")
if(!entry->hidden)
{
result.append(readFromDesktopFile(path));
}
if(suffix == "qsrun")
{
result.append(readFromFile(path));
entry->entryPath = path;
result.append(*entry);
}
}
}
@ -225,6 +290,73 @@ QVector<EntryConfig> EntryProvider::getSystemEntries()
return readConfig(this->systemEntriesDirsPaths);
}
void EntryProvider::saveUserEntry(const EntryConfig &config)
{
if(!isSavable(config))
{
throw std::runtime_error("Only user/inherited entries can be saved");
}
QString transitPath = config.entryPath + ".transit";
QFile file{transitPath};
if(!file.open(QIODevice::WriteOnly))
{
throw std::runtime_error("Error: Can not open file for writing");
}
QTextStream outStream(&file);
outStream << "type" << " " << ((config.type == EntryType::USER) ? "user" : "inherit") << Qt::endl;
if(!config.inherit.isEmpty())
{
outStream << "inherit" << " " << config.inherit << Qt::endl;
}
outStream << "row" << " " << config.row << Qt::endl;
outStream << "col" << " " << config.col << Qt::endl;
outStream << "hidden" << " " << config.hidden << Qt::endl;
if(!config.key.isEmpty())
{
outStream << "key" << " " << config.key << Qt::endl;
}
if(config.type == EntryType::USER)
{
if(!config.name.isEmpty())
{
outStream << "name" << " " << config.name << Qt::endl;
}
if(!config.command.isEmpty())
{
outStream << "command" << " " << config.command << Qt::endl;
}
if(!config.iconPath.isEmpty())
{
outStream << "icon" << " " << config.iconPath << Qt::endl;
}
if(!config.arguments.empty())
{
outStream << "arguments" << " " << config.arguments.join(' ') << Qt::endl;
}
}
outStream.flush();
file.close();
// Qts don't work if file already exists and c++17... don't want to pull in the fs lib yet
int ret = rename(transitPath.toStdString().c_str(), config.entryPath.toStdString().c_str());
if(ret != 0)
{
qDebug() << strerror(errno);
throw std::runtime_error("Failed to save entry file: Error during rename");
}
}
bool EntryProvider::deleteUserEntry(const EntryConfig &config)
{
if(!isSavable(config))
{
throw std::runtime_error("Only user/inherited entries can be deleted");
}
QFile file{config.entryPath};
return file.remove();
}
template <class T> void assignIfDestDefault(T &dest, const T &source)
{
if(dest == T())
@ -238,12 +370,16 @@ EntryConfig &EntryConfig::update(const EntryConfig &o)
assignIfDestDefault(this->arguments, o.arguments);
assignIfDestDefault(this->col, o.col);
assignIfDestDefault(this->command, o.command);
if(this->icon.isNull())
if(this->iconPath.isEmpty())
{
this->icon = o.icon;
this->iconPath = o.iconPath;
}
assignIfDestDefault(this->key, o.key);
assignIfDestDefault(this->name, o.name);
assignIfDestDefault(this->row, o.row);
assignIfDestDefault(this->hidden, o.hidden);
assignIfDestDefault(this->inherit, o.inherit);
assignIfDestDefault(this->entryPath, o.entryPath);
assignIfDestDefault(this->type, o.type);
return *this;
}

View File

@ -2,6 +2,7 @@
#define ENTRYPROVIDER_H
#include <QIcon>
#include <QSettings>
#include <optional>
class ConfigFormatException : public std::runtime_error
{
@ -14,14 +15,27 @@ class ConfigFormatException : public std::runtime_error
}
};
enum EntryType
{
USER,
INHERIT,
SYSTEM,
DYNAMIC
};
class EntryConfig
{
public:
EntryType type = SYSTEM;
bool hidden = false;
bool isTerminalCommand = false;
QString entryPath;
QString key;
QString name;
QString command;
QString iconPath;
QStringList arguments;
QIcon icon;
QString inherit;
int row = 0;
int col = 0;
@ -34,15 +48,19 @@ class EntryProvider
QStringList _desktopIgnoreArgs;
QStringList userEntriesDirsPaths;
QStringList systemEntriesDirsPaths;
EntryConfig readFromFile(const QString &path);
EntryConfig readqsrunFile(const QString &path);
EntryConfig readFromDesktopFile(const QString &path);
std::optional<EntryConfig> readEntryFromPath(const QString &path);
QVector<EntryConfig> readConfig(QStringList paths);
QString resolveEntryPath(QString path);
public:
EntryProvider(QStringList userEntriesDirsPaths, QStringList systemEntriesDirsPaths);
bool isSavable(const EntryConfig &config) const;
QVector<EntryConfig> getUserEntries();
QVector<EntryConfig> getSystemEntries();
void saveUserEntry(const EntryConfig &config);
bool deleteUserEntry(const EntryConfig &config);
};
#endif // ENTRYPROVIDER_H

View File

@ -13,39 +13,56 @@
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
#include <QDrag>
#include <QMimeData>
#include <QApplication>
#include "entrypushbutton.h"
EntryPushButton::EntryPushButton(const EntryConfig &config) : QPushButton()
{
this->setText(config.name);
this->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
this->setIcon(config.icon);
if(!config.icon.availableSizes().isEmpty())
QIcon icon;
if(config.isTerminalCommand && config.iconPath.isEmpty())
{
auto sizes = config.icon.availableSizes();
QSize maxSize = sizes.first();
for(QSize &current : sizes)
icon = resolveIcon("utilities-terminal");
}
else
{
if(current.width() > maxSize.width())
{
maxSize = current;
}
}
this->setIconSize(maxSize);
icon = resolveIcon(config.iconPath);
}
this->setIcon(icon);
this->setIconSize(QSize{256, 256});
this->config = config;
connect(this, SIGNAL(clicked()), this, SLOT(emitOwnClicked()));
systemEntryMenu.addAction("Add to favorites", [&] { emit addToFavourites(this->config); });
userEntryMenu.addAction("Delete", [&] { emit deleteRequested(this->config); });
}
QIcon EntryPushButton::resolveIcon(QString path)
{
if(!path.isEmpty())
{
if(path[0] == '/')
{
return QIcon(path);
}
else
{
return QIcon::fromTheme(path);
}
}
return QIcon();
}
void EntryPushButton::emitOwnClicked()
{
emit clicked(this->config);
}
const EntryConfig &EntryPushButton::getEntryConfig()
const EntryConfig &EntryPushButton::getEntryConfig() const
{
return this->config;
}
@ -65,12 +82,82 @@ void EntryPushButton::showName()
this->setText(this->config.name);
}
int EntryPushButton::getRow() const { return config.row; }
int EntryPushButton::getCol() const { return config.col; }
QString EntryPushButton::getName() const { return config.name; }
QString EntryPushButton::getShortcutKey() const { return config.key; }
void EntryPushButton::setShortcutKey(QString key) { this->config.key = key; }
void EntryPushButton::setRow(int row) { this->config.row = row; }
void EntryPushButton::setCol(int col) { this->config.col = col; }
QStringList EntryPushButton::getArguments() const { return this->config.arguments; }
QString EntryPushButton::getCommand() const { return this->config.command; }
void EntryPushButton::mousePressEvent(QMouseEvent *event)
{
if(event->button() == Qt::LeftButton)
{
dragStartPosition = event->pos();
}
if(event->button() == Qt::RightButton)
{
if(this->config.type == EntryType::USER || this->config.type == EntryType::INHERIT)
{
this->userEntryMenu.exec(QCursor::pos());
}
else if(this->config.type == EntryType::SYSTEM)
{
this->systemEntryMenu.exec(QCursor::pos());
}
}
return QPushButton::mousePressEvent(event);
}
void EntryPushButton::mouseMoveEvent(QMouseEvent *event)
{
if(this->config.type == EntryType::SYSTEM || this->config.type == EntryType::DYNAMIC)
{
return;
}
if(!(event->buttons() & Qt::LeftButton))
{
return;
}
if((event->pos() - dragStartPosition).manhattanLength() < QApplication::startDragDistance())
{
return;
}
QDrag *drag = new QDrag(this);
QMimeData *mimeData = new QMimeData();
QByteArray data;
mimeData->setData(ENTRYBUTTON_MIME_TYPE_STR, data);
drag->setMimeData(mimeData);
Qt::DropAction dropAction = drag->exec(Qt::MoveAction);
}
int EntryPushButton::getRow() const
{
return config.row;
}
int EntryPushButton::getCol() const
{
return config.col;
}
QString EntryPushButton::getName() const
{
return config.name;
}
QString EntryPushButton::getShortcutKey() const
{
return config.key;
}
void EntryPushButton::setShortcutKey(QString key)
{
this->config.key = key;
}
void EntryPushButton::setRow(int row)
{
this->config.row = row;
}
void EntryPushButton::setCol(int col)
{
this->config.col = col;
}
QStringList EntryPushButton::getArguments() const
{
return this->config.arguments;
}
QString EntryPushButton::getCommand() const
{
return this->config.command;
}

View File

@ -18,20 +18,38 @@
#include <QWidget>
#include <QPushButton>
#include <QMouseEvent>
#include <QMenu>
#include "entryprovider.h"
#define ENTRYBUTTON_MIME_TYPE_STR "application/x-qsrun-entrypushbutton"
class EntryPushButton : public QPushButton
{
Q_OBJECT
private:
private:
QMenu systemEntryMenu;
QMenu userEntryMenu;
EntryConfig config;
private slots:
QPoint dragStartPosition;
private slots:
void emitOwnClicked();
signals:
signals:
void clicked(const EntryConfig &config);
public:
void deleteRequested(EntryConfig &config);
void addToFavourites(const EntryConfig &config);
protected:
void mousePressEvent(QMouseEvent *event);
void mouseMoveEvent(QMouseEvent *event);
QIcon resolveIcon(QString path);
public:
EntryPushButton(const EntryConfig &config);
const EntryConfig &getEntryConfig();
const EntryConfig &getEntryConfig() const;
void setEntryConfig(const EntryConfig &config);
void showShortcut();
void showName();
@ -46,6 +64,4 @@ public:
void setShortcutKey(QString key);
};
#endif // ENTRYPUSHBUTTON_H

View File

@ -14,6 +14,7 @@
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
#include <QApplication>
#include <QCommandLineParser>
#include <QFuture>
#include <QFutureWatcher>
#include <QtConcurrent/QtConcurrentRun>
@ -30,56 +31,76 @@ int main(int argc, char *argv[])
QApplication app(argc, argv);
QString configDirectoryPath;
QDir dir;
bool newInstanceRequested = false;
if(argc >= 2)
{
configDirectoryPath = QCoreApplication::arguments().at(1);
if(!dir.exists(configDirectoryPath))
QCommandLineParser parser;
parser.addOptions({
{"new-instance", "Launch a new instance, ignoring any running ones"},
{"config", "Use supplied config dir instead of default"},
});
parser.addHelpOption();
parser.process(app.arguments());
configDirectoryPath = parser.value("config");
newInstanceRequested = parser.isSet("new-instance");
if(!configDirectoryPath.isEmpty() && !dir.exists(configDirectoryPath))
{
QMessageBox::warning(nullptr, "Directory not found", configDirectoryPath + " was not found");
return 1;
}
}
else
if(configDirectoryPath.isEmpty())
{
configDirectoryPath = QDir::homePath() + "/.config/qsrun/";
}
qRegisterMetaType<QVector<QString> >("QVector<QString>");
qRegisterMetaType<QVector<QString>>("QVector<QString>");
if(!dir.exists(configDirectoryPath))
{
if(!dir.mkdir(configDirectoryPath))
{
QMessageBox::warning(nullptr, "Failed to create dir", configDirectoryPath + " was not found and could not be created!");
QMessageBox::warning(nullptr, "Failed to create dir",
configDirectoryPath + " was not found and could not be created!");
return 1;
}
}
QSettings settings(configDirectoryPath + "qsrun.config", QSettings::NativeFormat);
SettingsProvider settingsProvider { settings };
SettingsProvider settingsProvider{settings};
EntryProvider entryProvider(settingsProvider.userEntriesPaths(), settingsProvider.systemApplicationsEntriesPaths());
//TODO if setting single instance mode
SingleInstanceServer *server = nullptr;
bool singleInstanceMode = !newInstanceRequested && settingsProvider.singleInstanceMode();
if(singleInstanceMode)
{
QLocalSocket localSocket;
localSocket.connectToServer("/tmp/qsrun.socket");
SingleInstanceServer server;
localSocket.connectToServer(settingsProvider.socketPath());
if(localSocket.isOpen() && localSocket.isWritable())
{
QDataStream stream(&localSocket);
stream << (int)0x01; //maximize
stream << (int)0x01; // maximize
localSocket.flush();
localSocket.waitForBytesWritten();
localSocket.disconnectFromServer();
return 0;
}
else
{
if(!server.listen("/tmp/qsrun.socket"))
server = new SingleInstanceServer();
if(!server->listen(settingsProvider.socketPath()))
{
qDebug() << "Failed to listen on socket!";
return 1;
}
Window *w = new Window { entryProvider, settingsProvider };
QObject::connect(&server, &SingleInstanceServer::receivedMaximizationRequest, [&w]{
}
Window *w = new Window{entryProvider, settingsProvider};
if(singleInstanceMode && server != nullptr)
{
QObject::connect(server, &SingleInstanceServer::receivedMaximizationRequest, [&w] {
if(w != nullptr)
{
qInfo() << "maximizing as requested by other instance";
@ -89,12 +110,10 @@ int main(int argc, char *argv[])
w->focusInput();
}
});
}
w->showMaximized();
w->focusInput();
}
return app.exec();
}

View File

@ -23,5 +23,5 @@ SOURCES += calculationengine.cpp \
QT += widgets sql network
QT_CONFIG -= no-pkg-config
LIBS += -lcln
CONFIG += link_pkgconfig
CONFIG += link_pkgconfig c++17
PKGCONFIG += libqalculate

BIN
screenshots/calc.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

BIN
screenshots/startview.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 KiB

View File

@ -1,6 +1,6 @@
#include "settingsprovider.h"
#include <QFileInfo>
#include <QDir>
#include <QFileInfo>
SettingsProvider::SettingsProvider(QSettings &settings)
{
@ -9,9 +9,9 @@ SettingsProvider::SettingsProvider(QSettings &settings)
QStringList SettingsProvider::userEntriesPaths() const
{
//TODO: make it configurable, but we stick with this for now.
// TODO: make it configurable, but we stick with this for now.
QFileInfo fi(this->settings->fileName());
return { fi.absoluteDir().absolutePath() };
return {fi.absoluteDir().absolutePath()};
}
QStringList SettingsProvider::systemApplicationsEntriesPaths() const
@ -19,9 +19,22 @@ QStringList SettingsProvider::systemApplicationsEntriesPaths() const
return settings->value("sysAppsPaths", "/usr/share/applications/").toStringList();
}
int SettingsProvider::getMaxCols() const
{
return settings->value("maxColumns", 3).toInt();
}
bool SettingsProvider::singleInstanceMode() const
{
return settings->value("singleInstance", true).toBool();
}
QString SettingsProvider::getTerminalCommand() const
{
return settings->value("terminal", "/usr/bin/x-terminal-emulator -e %c").toString();
}
QString SettingsProvider::socketPath() const
{
return settings->value("singleInstanceSocket", "/tmp/qsrun").toString();
}

View File

@ -1,18 +1,22 @@
#ifndef SETTINGSPROVIDER_H
#define SETTINGSPROVIDER_H
#include <stdexcept>
#include <QSettings>
#include <stdexcept>
class SettingsProvider
{
private:
private:
QSettings *settings;
public:
public:
SettingsProvider(QSettings &settings);
virtual QStringList userEntriesPaths() const;
virtual QStringList systemApplicationsEntriesPaths() const;
virtual int getMaxCols() const;
virtual bool singleInstanceMode() const;
QString getTerminalCommand() const;
QString socketPath() const;
};
#endif // SETTINGSPROVIDER_H

View File

@ -1,34 +1,35 @@
/*
* Copyright (c) 2018-2019 Albert S. <mail at quitesimple dot org>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
#include <QProcess>
#include <QProcessEnvironment>
* Copyright (c) 2018-2020 Albert S. <mail at quitesimple dot org>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
#include <QClipboard>
#include <QDate>
#include <QDebug>
#include <QDesktopServices>
#include <QDirIterator>
#include <QFileIconProvider>
#include <QHeaderView>
#include <QIcon>
#include <QKeySequence>
#include <QLabel>
#include <QDate>
#include <QHeaderView>
#include <QDesktopServices>
#include <QFileIconProvider>
#include <QDebug>
#include <QMenu>
#include <QClipboard>
#include <QProcess>
#include <QProcessEnvironment>
#include <QScrollArea>
#include "window.h"
#include "entryprovider.h"
#include "window.h"
Window::Window(EntryProvider &entryProvider, SettingsProvider &configProvider)
{
this->entryProvider = &entryProvider;
@ -36,6 +37,7 @@ Window::Window(EntryProvider &entryProvider, SettingsProvider &configProvider)
createGui();
initFromConfig();
this->lineEdit->installEventFilter(this);
this->setAcceptDrops(true);
QFont font;
font.setPointSize(48);
font.setBold(true);
@ -43,13 +45,12 @@ Window::Window(EntryProvider &entryProvider, SettingsProvider &configProvider)
calculationResultLabel.setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
calculationResultLabel.setAlignment(Qt::AlignCenter);
calculationResultLabel.setContextMenuPolicy(Qt::ContextMenuPolicy::CustomContextMenu);
connect(&calculationResultLabel, &QLabel::customContextMenuRequested, this, &Window::showCalculationResultContextMenu);
connect(&calculationResultLabel, &QLabel::customContextMenuRequested, this,
&Window::showCalculationResultContextMenu);
}
Window::~Window()
{
}
void Window::initFromConfig()
@ -77,9 +78,9 @@ void Window::showCalculationResultContextMenu(const QPoint &point)
menu.exec(QCursor::pos());
}
QVector<EntryPushButton*> Window::generateEntryButtons(const QVector<EntryConfig> &configs)
QVector<EntryPushButton *> Window::generateEntryButtons(const QVector<EntryConfig> &configs)
{
QVector<EntryPushButton*> result;
QVector<EntryPushButton *> result;
for(const EntryConfig &config : configs)
{
EntryPushButton *button = createEntryButton(config);
@ -117,12 +118,65 @@ void Window::populateGrid(const QVector<EntryPushButton *> &list)
}
}
void Window::buttonClick(const EntryPushButton &config)
void Window::executeConfig(const EntryConfig &config)
{
QProcess::startDetached(config.getCommand(), config.getArguments());
if(config.isTerminalCommand || QApplication::keyboardModifiers().testFlag(Qt::ShiftModifier))
{
QString cmd = settingsProvider->getTerminalCommand();
cmd.replace("%c", config.command);
QStringList args = QProcess::splitCommand(cmd);
QProcess::startDetached(args[0], args);
}
else
{
QProcess::startDetached(config.command, config.arguments);
}
this->closeWindow();
}
void Window::addToFavourites(const EntryConfig &config)
{
std::pair<int, int> cell = getNextFreeCell();
EntryConfig userConfig;
userConfig.type = EntryType::INHERIT;
userConfig.row = cell.first;
userConfig.col = cell.second;
userConfig.inherit = config.entryPath;
QFileInfo fi{config.entryPath};
QString entryName = fi.completeBaseName() + ".qsrun";
QString entryPath;
if(config.type == EntryType::SYSTEM)
{
entryPath = this->settingsProvider->userEntriesPaths()[0] + "/" + entryName;
}
else
{
entryPath = fi.absoluteDir().absoluteFilePath(entryName);
}
userConfig.entryPath = entryPath;
try
{
entryProvider->saveUserEntry(userConfig);
}
catch(std::exception &e)
{
QMessageBox::critical(this, "Failed to save item to favourites", e.what());
return;
}
/*we only want to save a minimal, inherited config. but it should be a "complete" button
when we add it to the favourites. the alternative would be to reload the whole config,
but that's probably overkill. */
userConfig.update(config);
userConfig.key = "";
userEntryButtons.append(createEntryButton(userConfig));
}
void Window::deleteEntry(EntryConfig &config)
{
this->entryProvider->deleteUserEntry(config);
initFromConfig();
}
void Window::closeWindow()
{
if(settingsProvider->singleInstanceMode())
@ -136,6 +190,48 @@ void Window::closeWindow()
}
}
std::pair<int, int> Window::getNextFreeCell()
{
/* Not the most efficient way perhaps but for now it'll do */
std::sort(userEntryButtons.begin(), userEntryButtons.end(), [](EntryPushButton *a, EntryPushButton *b) {
const EntryConfig &config_a = a->getEntryConfig();
const EntryConfig &config_b = b->getEntryConfig();
if(config_a.row < config_b.row)
{
return true;
}
if(config_a.row > config_b.row)
{
return false;
}
return config_a.col < config_b.col;
});
int expectedRow = 1;
int expectedCol = 1;
int maxCols = this->settingsProvider->getMaxCols();
for(EntryPushButton *current : userEntryButtons)
{
int currentRow = current->getEntryConfig().row;
int currentCol = current->getEntryConfig().col;
if(currentRow != expectedRow || currentCol != expectedCol)
{
return {expectedRow, expectedCol};
}
if(expectedCol == maxCols)
{
expectedCol = 1;
++expectedRow;
}
else
{
++expectedCol;
}
}
return {expectedRow, expectedCol};
}
QStringList Window::generatePATHSuggestions(const QString &text)
{
QStringList results;
@ -167,10 +263,11 @@ void Window::addPATHSuggestion(const QString &text)
{
EntryConfig e;
e.name = suggestions[0];
e.col=0;
e.row=0;
e.col = 0;
e.row = 0;
e.command = suggestions[0];
e.icon = QIcon::fromTheme(suggestions[0]);
e.iconPath = suggestions[0];
e.type = EntryType::DYNAMIC;
EntryPushButton *button = createEntryButton(e);
clearGrid();
grid->addWidget(button, 0, 0);
@ -198,7 +295,6 @@ void Window::addCalcResult(const QString &expression)
calculationResultLabel.setText(labelText);
calculationResultLabel.setVisible(true);
QFont currentFont = calculationResultLabel.font();
int calculatedPointSize = currentFont.pointSize();
QFontMetrics fm(currentFont);
@ -221,7 +317,7 @@ void Window::addCalcResult(const QString &expression)
grid->addWidget(&calculationResultLabel, 0, 0);
}
//main problem here there is no easy event compression (clearing emit queue and only processing the last one)
// main problem here there is no easy event compression (clearing emit queue and only processing the last one)
void Window::lineEditTextChanged(QString text)
{
if(text.length() >= 2)
@ -248,7 +344,8 @@ void Window::lineEditTextChanged(QString text)
e.arguments = arguments.mid(1);
}
e.command = arguments[0];
e.icon = QIcon::fromTheme("utilities-terminal");
e.iconPath = "utilities-terminal";
e.type = EntryType::DYNAMIC;
EntryPushButton *button = createEntryButton(e);
clearGrid();
@ -268,11 +365,11 @@ void Window::keyReleaseEvent(QKeyEvent *event)
}
}
QWidget::keyReleaseEvent(event);
}
void Window::keyPressEvent(QKeyEvent *event)
{
bool closeWindow = ((event->modifiers() & Qt::ControlModifier && event->key() == Qt::Key_Q) || event->key() == Qt::Key_Escape);
bool closeWindow =
((event->modifiers() & Qt::ControlModifier && event->key() == Qt::Key_Q) || event->key() == Qt::Key_Escape);
if(closeWindow)
{
this->closeWindow();
@ -287,22 +384,47 @@ void Window::keyPressEvent(QKeyEvent *event)
}
for(EntryPushButton *button : buttonsInGrid)
{
if(!button->getEntryConfig().key.isEmpty())
{
button->showShortcut();
}
}
QKeySequence seq(event->key());
QString key = seq.toString().toLower();
auto it = std::find_if(buttonsInGrid.begin(), buttonsInGrid.end(), [&key](const EntryPushButton *y) { return y->getShortcutKey() == key; });
auto it = std::find_if(buttonsInGrid.begin(), buttonsInGrid.end(),
[&key](const EntryPushButton *y) { return y->getShortcutKey() == key; });
if(it != buttonsInGrid.end())
{
buttonClick(**it);
executeConfig((*it)->getEntryConfig());
}
}
QWidget::keyPressEvent(event);
}
int Window::rankConfig(const EntryConfig &config, QString filter) const
{
if(config.name.startsWith(filter, Qt::CaseInsensitive))
{
return 0;
}
else if(config.command.startsWith(filter, Qt::CaseInsensitive))
{
return 1;
}
else if(config.name.contains(filter, Qt::CaseInsensitive))
{
return 2;
}
else if(config.command.contains(filter, Qt::CaseInsensitive))
{
return 3;
}
return -1;
}
void Window::filterGridFor(QString filter)
{
if(filter.length() > 0)
@ -311,7 +433,8 @@ void Window::filterGridFor(QString filter)
bool userEntryMatch = false;
for(EntryPushButton *button : this->userEntryButtons)
{
if(button->getName().contains(filter, Qt::CaseInsensitive))
if(button->getName().contains(filter, Qt::CaseInsensitive) ||
button->getCommand().contains(filter, Qt::CaseInsensitive))
{
button->setVisible(true);
grid->addWidget(button, button->getRow(), button->getCol());
@ -321,13 +444,27 @@ void Window::filterGridFor(QString filter)
}
if(!userEntryMatch)
{
QVector<RankedButton> rankedEntries;
int currow = 0;
int curcol = 0;
int i = 1;
const int MAX_COLS = this->settingsProvider->getMaxCols();
for(EntryPushButton *button : this->systemEntryButtons)
{
if(button->getName().contains(filter, Qt::CaseInsensitive))
int ranking = rankConfig(button->getEntryConfig(), filter);
if(ranking > -1)
{
RankedButton rb;
rb.button = button;
rb.ranking = ranking;
rankedEntries.append(rb);
}
}
std::sort(rankedEntries.begin(), rankedEntries.end(),
[](const RankedButton &a, const RankedButton &b) -> bool { return a.ranking < b.ranking; });
for(RankedButton &rankedButton : rankedEntries)
{
EntryPushButton *button = rankedButton.button;
button->setVisible(true);
if(i < 10)
{
@ -335,7 +472,7 @@ void Window::filterGridFor(QString filter)
}
grid->addWidget(button, currow, curcol++);
this->buttonsInGrid.append(button);
if(curcol == 3)
if(curcol == MAX_COLS)
{
curcol = 0;
++currow;
@ -343,21 +480,18 @@ void Window::filterGridFor(QString filter)
}
}
}
}
else
{
populateGrid(this->userEntryButtons);
}
}
EntryPushButton * Window::createEntryButton(const EntryConfig &entry)
EntryPushButton *Window::createEntryButton(const EntryConfig &entry)
{
EntryPushButton *button = new EntryPushButton(entry);
connect(button, &EntryPushButton::clicked, this, &Window::buttonClick);
connect(button, &EntryPushButton::clicked, this, &Window::executeConfig);
connect(button, &EntryPushButton::addToFavourites, this, &Window::addToFavourites);
connect(button, &EntryPushButton::deleteRequested, this, &Window::deleteEntry);
return button;
}
@ -370,10 +504,9 @@ void Window::lineEditReturnPressed()
return;
}
if(buttonsInGrid.length() > 0 && this->lineEdit->text().length() > 0 )
if(buttonsInGrid.length() > 0 && this->lineEdit->text().length() > 0)
{
buttonClick(*buttonsInGrid[0]);
executeConfig(buttonsInGrid[0]->getEntryConfig());
return;
}
}
@ -387,7 +520,7 @@ bool Window::eventFilter(QObject *obj, QEvent *event)
{
if(obj == this->lineEdit)
{
if (event->type() == QEvent::KeyPress)
if(event->type() == QEvent::KeyPress)
{
QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
if(keyEvent->key() == Qt::Key_Tab)
@ -396,12 +529,11 @@ bool Window::eventFilter(QObject *obj, QEvent *event)
if(suggestions.length() == 1)
{
this->lineEdit->setText(suggestions[0] + " ");
this->lineEdit->setCursorPosition(this->lineEdit->text().length()+1);
this->lineEdit->setCursorPosition(this->lineEdit->text().length() + 1);
}
return true;
}
}
}
return QObject::eventFilter(obj, event);
}
@ -410,3 +542,49 @@ void Window::focusInput()
{
this->lineEdit->setFocus();
}
void Window::dragEnterEvent(QDragEnterEvent *event)
{
if(event->mimeData()->hasFormat(ENTRYBUTTON_MIME_TYPE_STR))
{
event->acceptProposedAction();
}
}
void Window::dropEvent(QDropEvent *event)
{
int count = grid->count();
for(int i = 0; i < count; i++)
{
QLayoutItem *current = grid->itemAt(i);
if(current->geometry().contains(event->pos()))
{
EntryPushButton *buttonAtDrop = (EntryPushButton *)current->widget();
EntryPushButton *buttonAtSource = (EntryPushButton *)event->source();
int tmp_row = buttonAtSource->getRow();
int tmp_col = buttonAtSource->getCol();
grid->addWidget(buttonAtSource, buttonAtDrop->getRow(), buttonAtDrop->getCol());
buttonAtSource->setRow(buttonAtDrop->getRow());
buttonAtSource->setCol(buttonAtDrop->getCol());
grid->addWidget(buttonAtDrop, tmp_row, tmp_col);
buttonAtDrop->setRow(tmp_row);
buttonAtDrop->setCol(tmp_col);
try
{
this->entryProvider->saveUserEntry(buttonAtDrop->getEntryConfig());
this->entryProvider->saveUserEntry(buttonAtSource->getEntryConfig());
}
catch(std::exception &e)
{
QMessageBox::critical(this, "Failed to rearrange items", e.what());
}
break;
}
}
event->acceptProposedAction();
}

View File

@ -30,20 +30,34 @@
#include <QThread>
#include <QTreeWidget>
#include <QLabel>
#include <QMimeData>
#include <QDebug>
#include <QRect>
#include "entrypushbutton.h"
#include "calculationengine.h"
#include "settingsprovider.h"
class RankedButton
{
public:
EntryPushButton *button = nullptr;
int ranking;
};
class Window : public QWidget
{
Q_OBJECT
private:
protected:
void dragEnterEvent(QDragEnterEvent *event);
void dropEvent(QDropEvent *event);
private:
EntryProvider *entryProvider;
SettingsProvider *settingsProvider;
CalculationEngine calcEngine;
QString calculationresult;
QVector<EntryPushButton*> userEntryButtons;
QVector<EntryPushButton*> systemEntryButtons;
QVector<EntryPushButton *> userEntryButtons;
QVector<EntryPushButton *> systemEntryButtons;
QVector<EntryPushButton *> buttonsInGrid;
QLabel calculationResultLabel;
QString currentCalculationResult;
@ -56,27 +70,31 @@ private:
void keyReleaseEvent(QKeyEvent *event);
QVector<EntryPushButton *> generateEntryButtons(const QVector<EntryConfig> &userEntryButtons);
void keyPressEvent(QKeyEvent *event);
void buttonClick(const EntryPushButton &config);
void executeConfig(const EntryConfig &button);
void addToFavourites(const EntryConfig &button);
void deleteEntry(EntryConfig &config);
QLineEdit *lineEdit;
QGridLayout *grid;
EntryPushButton *createEntryButton(const EntryConfig &config);
void lineEditTextChanged(QString text);
void addPATHSuggestion(const QString &text);
void clearGrid();
void addCalcResult(const QString & expression);
void addCalcResult(const QString &expression);
void initTreeWidgets();
QStringList generatePATHSuggestions(const QString &text);
void closeWindow();
private slots:
std::pair<int, int> getNextFreeCell();
int rankConfig(const EntryConfig &config, QString filter) const;
private slots:
void lineEditReturnPressed();
void showCalculationResultContextMenu(const QPoint &point);
public:
public:
Window(EntryProvider &entryProvider, SettingsProvider &settingsProvider);
void setSystemConfig(const QVector<EntryConfig> &config);
bool eventFilter(QObject *obj, QEvent *event);
void focusInput();
~Window();
};
#endif