Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions inc/liquid.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@ class Liquid
static void applyQtStyleSheets(QWidget* widget);
static void createDesktopFile(const QString liquidAppName, const QString liquidAppStartingUrl);
static bool detectDarkMode(void);
static bool detectDarkModeUncached(void);
static QByteArray generateRandomByteArray(const int byteLength);
static QDir getAppsDir(void);
static QDir getConfigDir(void);
static QString getDefaultUserAgentString(void);
static QStringList getLiquidAppsList(void);
static void raiseWindow(QWidget* window);
static QString getUniqueFilePath(const QString dirPath, const QString baseName, const QString extension);
static void removeDesktopFile(const QString liquidAppName);
static void runLiquidApp(const QString liquidAppName);
Expand Down
2 changes: 2 additions & 0 deletions inc/liquidappwindow.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#include <QNetworkProxy>
#include <QSettings>
#include <QShortcut>
#include <QTimer>
#include <QWebEngineView>
#include <QWebEngineFullScreenRequest>

Expand Down Expand Up @@ -74,6 +75,7 @@ public slots:
QWebEngineProfile* liquidAppWebProfile = Q_NULLPTR;
QWebEngineSettings* liquidAppWebSettings = Q_NULLPTR;
QByteArray liquidAppWindowGeometry;
QTimer* rememberGeometryTimer = Q_NULLPTR;
QList<qreal> zoomFactors;

bool liquidAppWindowTitleIsReadOnly = false;
Expand Down
7 changes: 7 additions & 0 deletions inc/lqd.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@
#define LQD_ZOOM_LVL_STEP 0.04
#define LQD_ZOOM_LVL_STEP_FINE (LQD_ZOOM_LVL_STEP / 10)

/* Instance IDs (for preventing duplicate instances, and for talking to running ones) */
#define LQD_INSTANCE_ID_MAIN "main"
#define LQD_INSTANCE_ID_APP_PREFIX "app/"

/* Messages that can be sent to running instances */
#define LQD_INSTANCE_MSG_RAISE "raise"

/* Textual icons */
#define LQD_ICON_ADD "➕"
#define LQD_ICON_EDIT "⚙"
Expand Down
33 changes: 33 additions & 0 deletions inc/singleinstance.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
#pragma once

#include <QByteArray>
#include <QObject>
#include <QString>

class QLocalServer;

// Ensures that only one instance (of the main window, or of each Liquid app) runs at a time,
// and lets other processes talk to the one that's running, using local sockets (named pipes on Windows).
// Nothing polls anything: the running instance only wakes up when another process connects to it.
class SingleInstance : public QObject
{
Q_OBJECT

public:
explicit SingleInstance(const QString& instanceId, QObject* parent = Q_NULLPTR);

// Attempts to become the running instance; returns false if another one is already running
bool claim(void);

// Delivers a message to the running instance; returns false if there's none running
static bool sendMessage(const QString& instanceId, const QByteArray& message);

signals:
void messageReceived(const QByteArray& message);

private:
static QString serverName(const QString& instanceId);

QString instanceId;
QLocalServer* server = Q_NULLPTR;
};
2 changes: 2 additions & 0 deletions liquid.pro
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ HEADERS += inc/lqd.h \
inc/liquidappwebpage.hpp \
inc/liquidappwindow.hpp \
inc/mainwindow.hpp \
inc/singleinstance.hpp \

SOURCES += src/liquid.cpp \
src/liquidappcookiejar.cpp \
Expand All @@ -37,6 +38,7 @@ SOURCES += src/liquid.cpp \
src/liquidappwindow.cpp \
src/main.cpp \
src/mainwindow.cpp \
src/singleinstance.cpp \

RESOURCES = res/resources.qrc

Expand Down
40 changes: 37 additions & 3 deletions src/liquid.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@
#include <QApplication>
#include <QCoreApplication>
#include <QDebug>
#include <QElapsedTimer>
#include <QFileInfo>
#include <QProcess>
#include <QSettings>
#include <QSysInfo>
#include <QTime>
#include <QTimer>
#include <QWebEngineProfile>

#if defined(Q_OS_MAC)
Expand Down Expand Up @@ -86,6 +88,18 @@ void Liquid::createDesktopFile(const QString liquidAppName, const QString liquid
}

bool Liquid::detectDarkMode(void)
{
// Detecting it may involve spawning a process, do it only once per program run
static int cachedResult = -1;

if (cachedResult < 0) {
cachedResult = detectDarkModeUncached() ? 1 : 0;
}

return cachedResult == 1;
}

bool Liquid::detectDarkModeUncached(void)
{
#if defined(Q_OS_LINUX)
QProcess process;
Expand Down Expand Up @@ -183,6 +197,19 @@ QString Liquid::getUniqueFilePath(const QString dirPath, const QString baseName,
return filePath;
}

void Liquid::raiseWindow(QWidget* window)
{
if (window == Q_NULLPTR) {
return;
}

// Un-minimize (without affecting full-screen or maximized state), then bring to front
window->setWindowState(window->windowState() & ~Qt::WindowMinimized);
window->show();
window->raise();
window->activateWindow();
}

void Liquid::removeDesktopFile(const QString liquidAppName)
{
#if defined(Q_OS_LINUX)
Expand All @@ -209,9 +236,16 @@ void Liquid::runLiquidApp(const QString liquidAppName)

void Liquid::sleep(const int ms)
{
const QTime proceedAfter = QTime::currentTime().addMSecs(ms);
// Keep processing events while waiting, but without spinning the CPU:
// block until an event arrives, with the timer guaranteeing one when it's time to stop waiting
QTimer timer;
timer.setSingleShot(true);
timer.start(ms);

QElapsedTimer elapsed;
elapsed.start();

while (QTime::currentTime() < proceedAfter) {
QCoreApplication::processEvents(QEventLoop::AllEvents, ms / 4);
while (elapsed.elapsed() < ms) {
QCoreApplication::processEvents(QEventLoop::AllEvents | QEventLoop::WaitForMoreEvents);
}
}
18 changes: 13 additions & 5 deletions src/liquidappwindow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,16 @@ LiquidAppWindow::LiquidAppWindow(const QString* name) : QWebEngineView()
setWindowIcon(QIcon(":/images/" PROG_NAME ".svg"));
#endif

// Remember window geometry only once it settles (see resizeEvent())
rememberGeometryTimer = new QTimer(this);
rememberGeometryTimer->setSingleShot(true);
rememberGeometryTimer->setInterval(200);
connect(rememberGeometryTimer, &QTimer::timeout, this, [this]() {
if (!isFullScreen()) {
liquidAppWindowGeometry = saveGeometry();
}
});

// Disable default QWebEngineView's context menu
setContextMenuPolicy(Qt::PreventContextMenu);

Expand Down Expand Up @@ -812,12 +822,10 @@ void LiquidAppWindow::onIconChanged(QIcon icon)

void LiquidAppWindow::resizeEvent(QResizeEvent* event)
{
// Remember window size (unless in full-screen mode)
// Remember window size (unless in full-screen mode),
// once it stops changing (e.g. the user is done dragging, or resize animation is over)
if (event->spontaneous() && !isFullScreen()) {
// Pause here to wait for any kind of window resize animations to finish
Liquid::sleep(200);

liquidAppWindowGeometry = saveGeometry();
rememberGeometryTimer->start();
}

QWebEngineView::resizeEvent(event);
Expand Down
68 changes: 33 additions & 35 deletions src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,32 +10,17 @@
#include "liquidappconfigwindow.hpp"
#include "liquidappwindow.hpp"
#include "mainwindow.hpp"
#include "singleinstance.hpp"

QTextStream cout(stdout);

static QSharedMemory* sharedMemory = Q_NULLPTR;
static SingleInstance* singleInstance = Q_NULLPTR;

LiquidAppWindow* liquidAppWindow;
MainWindow* mainWindow;

QString getUserName()
{
QString name = qgetenv("USER");

if (name.isEmpty()) {
name = qgetenv("USERNAME");
}

return name;
}

static void onSignalHandler(int signum)
{
if (sharedMemory) {
delete sharedMemory;
sharedMemory = Q_NULLPTR;
}

if (liquidAppWindow) {
liquidAppWindow->close();
delete liquidAppWindow;
Expand All @@ -58,9 +43,8 @@ int main(int argc, char **argv)
int ret = EXIT_SUCCESS;

#if defined(Q_OS_LINUX) || defined(Q_OS_MAC)
// Handle any further termination signals to ensure
// that the QSharedMemory block is deleted
// even if the process crashes
// Handle termination signals to make sure that
// Liquid app's settings get saved upon exit
signal(SIGHUP, onSignalHandler);
signal(SIGINT, onSignalHandler);
signal(SIGQUIT, onSignalHandler);
Expand Down Expand Up @@ -89,16 +73,22 @@ int main(int argc, char **argv)
QApplication app(argc, argv);

if (argc < 2) {
// Allow only one instance
sharedMemory = new QSharedMemory(getUserName() + "_Liquid");
if (!sharedMemory->create(4, QSharedMemory::ReadOnly)) {
delete sharedMemory;
qDebug().noquote() << QString("Only one instance of Liquid is allowed");
exit(EXIT_FAILURE);
// Allow only one instance, bring the already running one to front instead of starting another
singleInstance = new SingleInstance(LQD_INSTANCE_ID_MAIN);
if (!singleInstance->claim()) {
qDebug().noquote() << QString("Liquid is already running");
SingleInstance::sendMessage(LQD_INSTANCE_ID_MAIN, LQD_INSTANCE_MSG_RAISE);
goto done;
}

// Show main program window
mainWindow = new MainWindow;

QObject::connect(singleInstance, &SingleInstance::messageReceived, mainWindow, [](const QByteArray& message) {
if (message == LQD_INSTANCE_MSG_RAISE) {
Liquid::raiseWindow(mainWindow);
}
});
} else { // App name provided
// CLI flags and options
QCommandLineParser parser;
Expand Down Expand Up @@ -151,16 +141,23 @@ int main(int argc, char **argv)

// Attempt to load app settings from a config file
if (!parser.isSet(editAppDialogFlag) && tempAppSettings->contains(LQD_CFG_KEY_NAME_URL)) {
// // Allow only one instance
sharedMemory = new QSharedMemory(getUserName() + "_Liquid_app_" + liquidAppName);
if (!sharedMemory->create(4, QSharedMemory::ReadOnly)) {
delete sharedMemory;
qDebug().noquote() << QString("Only one instance of Liquid app “%1” is allowed").arg(liquidAppName);
exit(EXIT_FAILURE);
// Allow only one instance per Liquid app, bring the already running one to front instead of starting another
const QString instanceId = LQD_INSTANCE_ID_APP_PREFIX + liquidAppName;
singleInstance = new SingleInstance(instanceId);
if (!singleInstance->claim()) {
qDebug().noquote() << QString("Liquid app “%1” is already running").arg(liquidAppName);
SingleInstance::sendMessage(instanceId, LQD_INSTANCE_MSG_RAISE);
goto done;
}

// Found existing liquid app settings file, show it
liquidAppWindow = new LiquidAppWindow(&liquidAppName);

QObject::connect(singleInstance, &SingleInstance::messageReceived, liquidAppWindow, [](const QByteArray& message) {
if (message == LQD_INSTANCE_MSG_RAISE) {
Liquid::raiseWindow(liquidAppWindow);
}
});
} else {
// No such Liquid app found, open Liquid app creation dialog
LiquidAppConfigDialog LiquidAppConfigDialog(mainWindow, liquidAppName);
Expand Down Expand Up @@ -196,9 +193,10 @@ int main(int argc, char **argv)
ret = app.exec();

done:
if (sharedMemory != Q_NULLPTR) {
delete sharedMemory;
sharedMemory = Q_NULLPTR;
if (singleInstance != Q_NULLPTR) {
// Stops listening (and removes the socket file, where applicable)
delete singleInstance;
singleInstance = Q_NULLPTR;
}

return ret;
Expand Down
14 changes: 2 additions & 12 deletions src/mainwindow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,7 @@ MainWindow::MainWindow() : QScrollArea()
switch (LiquidAppConfigDialog.exec()) {
case QDialog::Accepted:
// Give some time to the filesystem before scanning for the newly created Liquid App
{
QTime proceedAfter = QTime::currentTime().addMSecs(100);
while (QTime::currentTime() < proceedAfter) {
QCoreApplication::processEvents(QEventLoop::AllEvents, 50);
}
}
Liquid::sleep(100);
flushTable();
populateTable();

Expand Down Expand Up @@ -214,12 +209,7 @@ void MainWindow::populateTable(void)
switch (LiquidAppConfigDialog.exec()) {
case QDialog::Accepted:
// Give some time to the filesystem before scanning for the newly created app
{
QTime proceedAfter = QTime::currentTime().addMSecs(100);
while (QTime::currentTime() < proceedAfter) {
QCoreApplication::processEvents(QEventLoop::AllEvents, 50);
}
}
Liquid::sleep(100);
flushTable();
populateTable();
break;
Expand Down
Loading
Loading