Skip to content

Commit 0ba7b6c

Browse files
authored
Enable app class independent usage (#203)
This is a pure extension and fully backwards compatible. * Adds support for running it as a pre-compiled library while still being able to choose my `QXxxApplication` class at build time. * Be able to decide at runtime whether to use the single instance stuff (without starting a server and so on).
1 parent 565ebd1 commit 0ba7b6c

12 files changed

+365
-19
lines changed

.github/workflows/main.yml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,12 @@ jobs:
8080
cmake . ${{ matrix.additional_arguments }}
8181
cmake --build .
8282
83+
- name: Build separate_object example with CMake
84+
working-directory: examples/separate_object/
85+
run: |
86+
cmake . ${{ matrix.additional_arguments }}
87+
cmake --build .
88+
8389
- name: Build windows_raise_widget example with CMake
8490
working-directory: examples/windows_raise_widget/
8591
run: |
@@ -107,6 +113,13 @@ jobs:
107113
qmake
108114
${{ matrix.make }}
109115
116+
- name: Build separate_object example with QMake
117+
if: ${{ !contains(matrix.platform, 'macos') }}
118+
working-directory: examples/separate_object/
119+
run: |
120+
qmake
121+
${{ matrix.make }}
122+
110123
- name: Build windows_raise_widget example with QMake
111124
if: ${{ !contains(matrix.platform, 'macos') }}
112125
working-directory: examples/windows_raise_widget/

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# Changelog
22

3+
## 3.6.0
4+
5+
* Freestanding mode where `SingleApplication` doesn't derive from `QCodeApplication` _Benjamin Buch_
6+
* CMake install with CMake config files for freestanding mode _Benjamin Buch_
7+
38
## 3.5.1
49

510
* Bug Fix: Maximum QNativeIpcKey key size on macOS. - _Jonas Kvinge_

CMakeLists.txt

Lines changed: 70 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,29 @@
11
cmake_minimum_required(VERSION 3.12.0)
22

3-
project(SingleApplication LANGUAGES CXX DESCRIPTION "Replacement for QtSingleApplication")
3+
project(SingleApplication VERSION 3.6.0 LANGUAGES CXX DESCRIPTION "Replacement for QtSingleApplication")
44

55
set(CMAKE_AUTOMOC ON)
66

77
add_library(${PROJECT_NAME} STATIC
88
singleapplication.cpp
99
singleapplication_p.cpp
1010
)
11-
add_library(${PROJECT_NAME}::${PROJECT_NAME} ALIAS ${PROJECT_NAME})
1211

12+
# User configurable options
1313
if(NOT QT_DEFAULT_MAJOR_VERSION)
1414
set(QT_DEFAULT_MAJOR_VERSION 5 CACHE STRING "Qt version to use (5 or 6), defaults to 5")
1515
endif()
1616

17+
if(NOT QAPPLICATION_CLASS)
18+
set(QAPPLICATION_CLASS QCoreApplication CACHE STRING "Qt application base class or FreeStandingSingleApplication")
19+
endif()
20+
21+
option(SINGLEAPPLICATION_INSTALL OFF "Enable freestanding mode install including config files")
22+
23+
if(SINGLEAPPLICATION_INSTALL AND NOT QAPPLICATION_CLASS STREQUAL "FreeStandingSingleApplication")
24+
message(FATAL_ERROR "SINGLEAPPLICATION_INSTALL requires QAPPLICATION_CLASS == FreeStandingSingleApplication")
25+
endif()
26+
1727
# Find dependencies
1828
set(QT_COMPONENTS Core Network)
1929
set(QT_LIBRARIES Qt${QT_DEFAULT_MAJOR_VERSION}::Core Qt${QT_DEFAULT_MAJOR_VERSION}::Network)
@@ -24,8 +34,6 @@ if(QAPPLICATION_CLASS STREQUAL QApplication)
2434
elseif(QAPPLICATION_CLASS STREQUAL QGuiApplication)
2535
list(APPEND QT_COMPONENTS Gui)
2636
list(APPEND QT_LIBRARIES Qt${QT_DEFAULT_MAJOR_VERSION}::Gui)
27-
else()
28-
set(QAPPLICATION_CLASS QCoreApplication)
2937
endif()
3038

3139
find_package(Qt${QT_DEFAULT_MAJOR_VERSION} COMPONENTS ${QT_COMPONENTS} REQUIRED)
@@ -41,8 +49,15 @@ if(WIN32)
4149
target_link_libraries(${PROJECT_NAME} PRIVATE advapi32)
4250
endif()
4351

44-
target_compile_definitions(${PROJECT_NAME} PUBLIC QAPPLICATION_CLASS=${QAPPLICATION_CLASS})
45-
target_include_directories(${PROJECT_NAME} PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
52+
if(SINGLEAPPLICATION_INSTALL)
53+
target_compile_definitions(${PROJECT_NAME} PRIVATE QAPPLICATION_CLASS=${QAPPLICATION_CLASS})
54+
target_include_directories(${PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
55+
target_include_directories(${PROJECT_NAME} INTERFACE $<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>)
56+
else()
57+
target_compile_definitions(${PROJECT_NAME} PUBLIC QAPPLICATION_CLASS=${QAPPLICATION_CLASS})
58+
target_include_directories(${PROJECT_NAME} PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
59+
endif()
60+
4661
target_compile_definitions(${PROJECT_NAME} PRIVATE
4762
QT_NO_CAST_TO_ASCII
4863
QT_NO_CAST_FROM_ASCII
@@ -81,3 +96,52 @@ if(DOXYGEN_FOUND)
8196
README.md
8297
)
8398
endif()
99+
100+
if(SINGLEAPPLICATION_INSTALL)
101+
# Create a header veriant where QAPPLICATION_CLASS is replaced with FreeStandingSingleApplication
102+
file(READ "${CMAKE_CURRENT_SOURCE_DIR}/singleapplication.h" SINGLEAPPLICATION_H_CONTENT)
103+
104+
string(REGEX REPLACE
105+
"#ifndef QAPPLICATION_CLASS[^\n]*\n[ \t]*#define QAPPLICATION_CLASS QCoreApplication[^\n]*\n[ \t]*#endif[^\n]*\n"
106+
""
107+
SINGLEAPPLICATION_H_CONTENT
108+
"${SINGLEAPPLICATION_H_CONTENT}")
109+
110+
string(REGEX REPLACE
111+
"#include QT_STRINGIFY\\(QAPPLICATION_CLASS\\)"
112+
"#include \"FreeStandingSingleApplication\""
113+
SINGLEAPPLICATION_H_CONTENT
114+
"${SINGLEAPPLICATION_H_CONTENT}")
115+
116+
string(REPLACE
117+
"QAPPLICATION_CLASS"
118+
"FreeStandingSingleApplication"
119+
SINGLEAPPLICATION_H_CONTENT
120+
"${SINGLEAPPLICATION_H_CONTENT}")
121+
122+
file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/singleapplication.h" "${SINGLEAPPLICATION_H_CONTENT}")
123+
124+
# CMake install
125+
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/singleapplication.h" "SingleApplication" "FreeStandingSingleApplication"
126+
DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}")
127+
128+
include(CMakePackageConfigHelpers)
129+
write_basic_package_version_file(
130+
"SingleApplicationConfigVersion.cmake"
131+
VERSION "${PACKAGE_VERSION}"
132+
COMPATIBILITY SameMajorVersion)
133+
134+
configure_file("SingleApplicationConfig.cmake.in" "SingleApplicationConfig.cmake" @ONLY)
135+
install(FILES
136+
"${CMAKE_CURRENT_BINARY_DIR}/SingleApplicationConfig.cmake"
137+
"${CMAKE_CURRENT_BINARY_DIR}/SingleApplicationConfigVersion.cmake"
138+
DESTINATION "lib/cmake/SingleApplication")
139+
140+
install(TARGETS SingleApplication EXPORT SingleApplicationTargets)
141+
install(EXPORT SingleApplicationTargets
142+
FILE "SingleApplicationTargets.cmake"
143+
NAMESPACE "SingleApplication::"
144+
DESTINATION "lib/cmake/SingleApplication")
145+
else()
146+
add_library(${PROJECT_NAME}::${PROJECT_NAME} ALIAS ${PROJECT_NAME})
147+
endif()

FreeStandingSingleApplication

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// Copyright (c) Itay Grudev 2015 - 2023
2+
//
3+
// Permission is hereby granted, free of charge, to any person obtaining a copy
4+
// of this software and associated documentation files (the "Software"), to deal
5+
// in the Software without restriction, including without limitation the rights
6+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7+
// copies of the Software, and to permit persons to whom the Software is
8+
// furnished to do so, subject to the following conditions:
9+
//
10+
// Permission is not granted to use this software or any of the associated files
11+
// as sample data for the purposes of building machine learning models.
12+
//
13+
// The above copyright notice and this permission notice shall be included in
14+
// all copies or substantial portions of the Software.
15+
//
16+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22+
// THE SOFTWARE.
23+
24+
#ifndef FREE_STANDING_SINGLE_APPLICATION_H
25+
#define FREE_STANDING_SINGLE_APPLICATION_H
26+
27+
#include <QCoreApplication>
28+
29+
/**
30+
* @brief Fake Qt application base class
31+
* Use this as base if you want to use SingleApplication as a free standing object that must be
32+
* explicitly instanciated after your Qt application object.
33+
*
34+
* This enables you to use SingleApplication as a precompiled library and/or to decide at runtime
35+
* if you want to use a SingleApplication instance or not.
36+
*/
37+
struct FreeStandingSingleApplication: QObject{
38+
FreeStandingSingleApplication( int&, char** ) {}
39+
};
40+
41+
#endif

README.md

Lines changed: 68 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,67 @@ The library uses `stdlib` to terminate the program with the `exit()` function.
119119
Also don't forget to specify which `QCoreApplication` class your app is using if it
120120
is not `QCoreApplication` as in examples above.
121121

122+
## Freestanding mode
123+
124+
Traditionally, the functionality of this library is implemented as part of the Qt
125+
application class. The base class is defined by the macro `QAPPLICATION_CLASS`.
126+
127+
In freestanding mode, `SingleApplication` is not derived from a Qt application
128+
class. Instead, an instance of a Qt application class is created as normal,
129+
followed by a separate instance of the `SingleApplication` class.
130+
131+
```cpp
132+
#include <QApplication>
133+
#include <SingleApplication.h>
134+
135+
int main( int argc, char* argv[] )
136+
{
137+
// The normal application class with a type of your choice
138+
QApplication app( argc, argv );
139+
140+
// Separate single application object (argc and argv are discarded)
141+
SingleApplication single( argc, argv /*, options ...*/ );
142+
143+
// Do your stuff
144+
145+
return app.exec();
146+
}
147+
```
148+
149+
_Note:_ With the discarded arguments and the class name that sounds like a Qt
150+
application class without being one, this looks like a workaround – it is a
151+
workaround. For 4.x, the single instance functionality could be moved to
152+
something like a `SingleManager` class, which would then be used to implement
153+
`SingleApplication`. This can't be done in 3.x, because moving
154+
`SingleApplication::Mode` to `SingleManager::Mode` would be a breaking change.
155+
156+
To enable the freestanding mode set `QAPPLICATION_CLASS` to
157+
`FreeStandingSingleApplication`. This is a fake base class with no additional
158+
functionality.
159+
160+
The standalone mode allows us to use a precompiled version of this library,
161+
because we don't need the `QAPPLICATION_CLASS` macro to define our Qt application
162+
class at build time. Furthermore, we can use `std::optional<SingleApplication>`
163+
to decide at runtime whether we want single application functionality or not.
164+
165+
Use the standard CMake workflow to create a precompiled static library version,
166+
including CMake config files.
167+
168+
```bash
169+
cmake -DQAPPLICATION_CLASS=FreeStandingSingleApplication -DSINGLEAPPLICATION_INSTALL=ON SingleApplicationDir
170+
cmake --build .
171+
cmake --install
172+
```
173+
174+
This can be used via:
175+
176+
```cmake
177+
find_package(SingleApplication REQUIRED)
178+
target_link_libraries(YourTarget SingleApplication::SingleApplication)
179+
```
180+
181+
_Note:_ The `QAPPLICATION_CLASS` macro is eliminated during CMake install.
182+
122183
## Instance started signal
123184

124185
The `SingleApplication` class implements a `instanceStarted()` signal. You can
@@ -185,11 +246,13 @@ will replace the Primary one even if the Secondary flag has been set.*
185246

186247
## Examples
187248

188-
There are three examples provided in this repository:
249+
There are five examples provided in this repository:
189250

190-
* Basic example that prevents a secondary instance from starting [`examples/basic`](https://github.com/itay-grudev/SingleApplication/tree/master/examples/basic)
191-
* An example of a graphical application raising it's parent window [`examples/calculator`](https://github.com/itay-grudev/SingleApplication/tree/master/examples/calculator)
192-
* A console application sending the primary instance it's command line parameters [`examples/sending_arguments`](https://github.com/itay-grudev/SingleApplication/tree/master/examples/sending_arguments)
251+
* Basic example that prevents a secondary instance from starting [`examples/basic`](examples/basic)
252+
* An example of a graphical application raising it's parent window [`examples/calculator`](examples/calculator)
253+
* A console application sending the primary instance it's command line parameters [`examples/sending_arguments`](examples/sending_arguments)
254+
* A variant of `sending_arguments` where `SingleApplication`is used in freestanding mode [`examples/separate_object`](examples/separate_object)
255+
* A graphical application with Windows specific additions raising it's parent window [`examples/windows_raise_widget`](examples/windows_raise_widget)
193256

194257
## Versioning
195258

@@ -212,7 +275,7 @@ instances running.
212275

213276
## License
214277

215-
This library and it's supporting documentation, with the exception of the Qt
278+
This library and it's supporting documentation, with the exception of the Qt
216279
calculator examples which is distributed under the BSD license, are released
217280
under the terms of `The MIT License (MIT)` with an extra condition, that:
218281

SingleApplicationConfig.cmake.in

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
include(CMakeFindDependencyMacro)
2+
3+
find_dependency(Qt@QT_DEFAULT_MAJOR_VERSION@ COMPONENTS Core Network REQUIRED)
4+
5+
include("${CMAKE_CURRENT_LIST_DIR}/SingleApplicationTargets.cmake")
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
cmake_minimum_required(VERSION 3.7.0)
2+
3+
project(separate_object LANGUAGES CXX)
4+
5+
set(CMAKE_AUTOMOC ON)
6+
7+
# SingleApplication base class
8+
set(QAPPLICATION_CLASS FreeStandingSingleApplication)
9+
add_subdirectory(../.. SingleApplication)
10+
11+
find_package(Qt${QT_DEFAULT_MAJOR_VERSION} COMPONENTS Core REQUIRED)
12+
13+
add_executable(${PROJECT_NAME}
14+
main.cpp
15+
messagereceiver.cpp
16+
messagereceiver.h
17+
main.cpp
18+
)
19+
20+
target_link_libraries(${PROJECT_NAME} SingleApplication::SingleApplication)

examples/separate_object/main.cpp

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// Copyright (c) Itay Grudev 2015 - 2023
2+
//
3+
// Permission is hereby granted, free of charge, to any person obtaining a copy
4+
// of this software and associated documentation files (the "Software"), to deal
5+
// in the Software without restriction, including without limitation the rights
6+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7+
// copies of the Software, and to permit persons to whom the Software is
8+
// furnished to do so, subject to the following conditions:
9+
//
10+
// Permission is not granted to use this software or any of the associated files
11+
// as sample data for the purposes of building machine learning models.
12+
//
13+
// The above copyright notice and this permission notice shall be included in
14+
// all copies or substantial portions of the Software.
15+
//
16+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22+
// THE SOFTWARE.
23+
24+
#include <singleapplication.h>
25+
#include "messagereceiver.h"
26+
27+
int main(int argc, char *argv[])
28+
{
29+
QCoreApplication app( argc, argv );
30+
31+
// Separate single instance object (that allows secondary instances)
32+
SingleApplication single_instance_guard( argc, argv, true );
33+
34+
MessageReceiver msgReceiver;
35+
36+
// If this is a secondary instance
37+
if( single_instance_guard.isSecondary() ) {
38+
single_instance_guard.sendMessage( app.arguments().join(' ').toUtf8() );
39+
qDebug() << "App already running.";
40+
qDebug() << "Primary instance PID: " << single_instance_guard.primaryPid();
41+
qDebug() << "Primary instance user: " << single_instance_guard.primaryUser();
42+
return 0;
43+
} else {
44+
QObject::connect(
45+
&single_instance_guard,
46+
&SingleApplication::receivedMessage,
47+
&msgReceiver,
48+
&MessageReceiver::receivedMessage
49+
);
50+
}
51+
52+
return app.exec();
53+
}

0 commit comments

Comments
 (0)