Subject: [PATCH] Support building the GUI against Qt4

Qt5 was never released for macOS older than 10.7, which leaves the GUI
unbuildable on legacy systems even though the core builds fine there.
The GUI is Synergy 1.x heritage and is still written almost entirely in
Qt4 idiom (module-wide includes, SIGNAL/SLOT, QRegExp), so supporting
Qt4 again only needs a handful of changes:

  * select the Qt major version at configure time (BARRIER_QT_VERSION,
    defaulting to Auto: Qt5 if present, otherwise Qt4)
  * QGuiApplication::platformName() is Qt5-only, and Qt4 predates
    Wayland anyway, so compile the warning out
  * QIcon::setIsMask() is Qt 5.6 and newer; without it the tray icon is
    just not treated as a template image on macOS
  * QTcpServer::incomingConnection() takes an int in Qt4 and a qintptr
    in Qt5
  * replace the two pointer-to-member-function connects, which Qt4 has
    no equivalent for, with the classic SIGNAL/SLOT form

The .ui files are all <ui version="4.0"> and use no post-Qt4 widgets,
and the translations are shipped pre-compiled, so uic/lrelease need no
attention.

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 86deb4b7..8efee283 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -23,6 +23,29 @@ option (BARRIER_BUILD_INSTALLER "Build the installer" ON)
 option (BARRIER_BUILD_TESTS "Build the tests" ON)
 option (BARRIER_USE_EXTERNAL_GTEST "Use external installation of Google Test framework" OFF)
 
+# Qt5 is the default. Qt4 is supported for platforms that Qt5 never targeted,
+# most notably macOS releases older than 10.7 (including PowerPC).
+set (BARRIER_QT_VERSION "Auto" CACHE STRING "Qt version to build the GUI against: Auto, 5 or 4")
+set_property (CACHE BARRIER_QT_VERSION PROPERTY STRINGS Auto 5 4)
+
+if (BARRIER_BUILD_GUI)
+    if (BARRIER_QT_VERSION STREQUAL "Auto")
+        find_package (Qt5 QUIET COMPONENTS Core Widgets Network)
+        if (Qt5_FOUND)
+            set (BARRIER_QT_VERSION 5)
+        else()
+            set (BARRIER_QT_VERSION 4)
+        endif()
+    endif()
+
+    if (NOT BARRIER_QT_VERSION STREQUAL "4" AND NOT BARRIER_QT_VERSION STREQUAL "5")
+        message (FATAL_ERROR
+                 "BARRIER_QT_VERSION must be Auto, 4 or 5 (got '${BARRIER_QT_VERSION}')")
+    endif()
+
+    message (STATUS "Building the GUI against Qt${BARRIER_QT_VERSION}")
+endif()
+
 set (CMAKE_EXPORT_COMPILE_COMMANDS ON)
 set (CMAKE_CXX_STANDARD 14)
 set (CMAKE_CXX_EXTENSIONS OFF)
diff --git a/clean_build.sh b/clean_build.sh
index 585ca88b..0a8985ee 100755
--- a/clean_build.sh
+++ b/clean_build.sh
@@ -15,6 +15,9 @@ if [ "$(uname)" = "Darwin" ]; then
     # run the osx_environment.sh script to fix paths
     . ./osx_environment.sh
     B_CMAKE_FLAGS="-DCMAKE_OSX_SYSROOT=$(xcode-select --print-path)/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk -DCMAKE_OSX_DEPLOYMENT_TARGET=10.9 $B_CMAKE_FLAGS"
+    if [ -n "$BARRIER_QT_VERSION" ]; then
+        B_CMAKE_FLAGS="-DBARRIER_QT_VERSION=$BARRIER_QT_VERSION $B_CMAKE_FLAGS"
+    fi
 fi
 # allow local customizations to build environment
 [ -r ./build_env.sh ] && . ./build_env.sh
diff --git a/dist/macos/bundle/build_dist.sh.in b/dist/macos/bundle/build_dist.sh.in
index 26a93201..101a26b5 100755
--- a/dist/macos/bundle/build_dist.sh.in
+++ b/dist/macos/bundle/build_dist.sh.in
@@ -12,6 +12,7 @@ B_BINDIR="@CMAKE_RUNTIME_OUTPUT_DIRECTORY@"
 B_BUILDTYPE="@CMAKE_BUILD_TYPE@"
 B_BARRIERC="Barrier.app/Contents/MacOS/barrierc"
 B_BARRIERS="Barrier.app/Contents/MacOS/barriers"
+B_QT_VERSION="@BARRIER_QT_VERSION@"
 
 # Colorized output
 info() { tput bold; echo "$@"; tput sgr0 ; }
@@ -35,12 +36,21 @@ info "Copying binaries into bundle"
 # Copy the folder instead of globbing unquoted path
 cp -r "$B_BINDIR" "$B_MACOS" || exit 1
 
+# macdeployqt ships in a different package depending on the Qt major version
+if [ "$B_QT_VERSION" = "4" ]; then
+    B_PORT_PACKAGE="qt4-mac"
+    B_BREW_PACKAGE="qt@4"
+else
+    B_PORT_PACKAGE="qt5-qttools"
+    B_BREW_PACKAGE="qt@5"
+fi
+
 # Check for macdeployqt on MacPorts
 if which -s port ; then
     info "MacPorts found, searching for macdeployqt"
-    DEPLOYQT="$(port contents qt5-qttools | grep --only --max-count 1 '/.*macdeployqt')"
+    DEPLOYQT="$(port contents $B_PORT_PACKAGE | grep --only --max-count 1 '/.*macdeployqt')"
     if [ ! -x "$DEPLOYQT" ]; then
-        error "Please install package qt5-qttools"
+        error "Please install package $B_PORT_PACKAGE"
         exit 1
     fi
 fi
@@ -48,9 +58,9 @@ fi
 # Check for macdeployqt on Homebrew
 if which -s brew ; then
     info "Homebrew found, searching for macdeployqt"
-    DEPLOYQT="$(brew list qt@5 | grep --only '/.*macdeployqt' | head -1)"
+    DEPLOYQT="$(brew list $B_BREW_PACKAGE | grep --only '/.*macdeployqt' | head -1)"
     if [ ! -x "$DEPLOYQT" ]; then
-        error "Please install package qt"
+        error "Please install package $B_BREW_PACKAGE"
         exit 1
     fi
 fi
diff --git a/osx_environment.sh b/osx_environment.sh
index 2ee886da..82a200fb 100644
--- a/osx_environment.sh
+++ b/osx_environment.sh
@@ -16,22 +16,60 @@ if [ -z "$BARRIER_BUILD_ENV" ]; then
 
     printf "Modifying environment for Barrier build...\n"
 
+    # Set BARRIER_QT_VERSION=4 to build the GUI against Qt4 instead. This is
+    # only useful on systems Qt5 never supported (macOS older than 10.7).
+    # Left unset, we prefer Qt5 and fall back to Qt4 if only Qt4 is installed.
+
     if command -v port; then
         printf "Detected Macports\n"
 
-        check_dir_exists '/opt/local/lib/cmake/Qt5' 'qt5-qtbase port'
+        if [ -z "$BARRIER_QT_VERSION" ]; then
+            if [ -d '/opt/local/lib/cmake/Qt5' ]; then
+                BARRIER_QT_VERSION=5
+            elif [ -x '/opt/local/libexec/qt4/bin/qmake' ]; then
+                BARRIER_QT_VERSION=4
+            else
+                check_dir_exists '/opt/local/lib/cmake/Qt5' 'qt5-qtbase port'
+            fi
+        fi
 
         export BARRIER_BUILD_MACPORTS=1
-        export CMAKE_PREFIX_PATH="/opt/local/lib/cmake/Qt5:$CMAKE_PREFIX_PATH"
         export LD_LIBRARY_PATH="/opt/local/lib:$LD_LIBRARY_PATH"
         export CPATH="/opt/local/include:$CPATH"
-        export PKG_CONFIG_PATH="/opt/local/libexec/qt5/lib/pkgconfig:$PKG_CONFIG_PATH"
+
+        if [ "$BARRIER_QT_VERSION" = 4 ]; then
+            check_dir_exists '/opt/local/libexec/qt4' 'qt4-mac port'
+            # FindQt4 locates everything else from qmake
+            export QT_QMAKE_EXECUTABLE='/opt/local/libexec/qt4/bin/qmake'
+            export PATH="/opt/local/libexec/qt4/bin:$PATH"
+            export PKG_CONFIG_PATH="/opt/local/libexec/qt4/lib/pkgconfig:$PKG_CONFIG_PATH"
+        else
+            check_dir_exists '/opt/local/lib/cmake/Qt5' 'qt5-qtbase port'
+            export CMAKE_PREFIX_PATH="/opt/local/lib/cmake/Qt5:$CMAKE_PREFIX_PATH"
+            export PKG_CONFIG_PATH="/opt/local/libexec/qt5/lib/pkgconfig:$PKG_CONFIG_PATH"
+        fi
 
     elif command -v brew; then
         printf "Detected Homebrew\n"
-        QT_PATH=$(brew --prefix qt@5)
 
-        check_dir_exists "$QT_PATH" 'qt5'
+        if [ -z "$BARRIER_QT_VERSION" ]; then
+            if brew --prefix qt@5 >/dev/null 2>&1; then
+                BARRIER_QT_VERSION=5
+            else
+                BARRIER_QT_VERSION=4
+            fi
+        fi
+
+        if [ "$BARRIER_QT_VERSION" = 4 ]; then
+            # homebrew-core dropped qt@4; this comes from a tap such as cartr/qt4
+            QT_PATH=$(brew --prefix qt@4 2>/dev/null)
+            check_dir_exists "$QT_PATH" 'qt4'
+            export QT_QMAKE_EXECUTABLE="$QT_PATH/bin/qmake"
+            export PATH="$QT_PATH/bin:$PATH"
+        else
+            QT_PATH=$(brew --prefix qt@5 2>/dev/null)
+            check_dir_exists "$QT_PATH" 'qt5'
+        fi
 
         export BARRIER_BUILD_BREW=1
         export CMAKE_PREFIX_PATH="/opt/procursus:$QT_PATH:$CMAKE_PREFIX_PATH"
@@ -43,6 +81,7 @@ if [ -z "$BARRIER_BUILD_ENV" ]; then
         exit 1
     fi
 
+    export BARRIER_QT_VERSION
     export BARRIER_BUILD_ENV=1
 
     printf "done\n"
diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt
index 570e8424..8934166a 100644
--- a/src/gui/CMakeLists.txt
+++ b/src/gui/CMakeLists.txt
@@ -1,6 +1,14 @@
 cmake_minimum_required (VERSION 3.4)
 
-find_package (Qt5 REQUIRED COMPONENTS Core Widgets Network)
+if (BARRIER_QT_VERSION STREQUAL "4")
+    # Qt4 has no separate Widgets module -- QtGui provides QWidget and friends.
+    find_package (Qt4 REQUIRED COMPONENTS QtCore QtGui QtNetwork)
+    set (BARRIER_QT_LIBRARIES Qt4::QtCore Qt4::QtGui Qt4::QtNetwork)
+else()
+    find_package (Qt5 REQUIRED COMPONENTS Core Widgets Network)
+    set (BARRIER_QT_LIBRARIES Qt5::Core Qt5::Widgets Qt5::Network)
+endif()
+
 set (CMAKE_AUTOMOC ON)
 set (CMAKE_AUTORCC ON)
 set (CMAKE_AUTOUIC ON)
@@ -131,7 +139,7 @@ add_executable (barrier WIN32
 
 include_directories (./src)
 
-target_link_libraries(barrier net base io Qt5::Core Qt5::Widgets Qt5::Network ${OPENSSL_LIBS})
+target_link_libraries(barrier net base io ${BARRIER_QT_LIBRARIES} ${OPENSSL_LIBS})
 target_compile_definitions (barrier PRIVATE -DBARRIER_VERSION_STAGE="${BARRIER_VERSION_STAGE}")
 target_compile_definitions (barrier PRIVATE -DBARRIER_REVISION="${BARRIER_REVISION}")
 
@@ -177,5 +185,5 @@ if (BARRIER_BUILD_TESTS)
     add_test(guiunittests guiunittests)
 
     target_include_directories(guiunittests PUBLIC ../../ext)
-    target_link_libraries(guiunittests gtest gmock Qt5::Core Qt5::Widgets Qt5::Network ${libs})
+    target_link_libraries(guiunittests gtest gmock ${BARRIER_QT_LIBRARIES} ${libs})
 endif()
diff --git a/src/gui/src/MainWindow.cpp b/src/gui/src/MainWindow.cpp
index d17548a4..71875c72 100644
--- a/src/gui/src/MainWindow.cpp
+++ b/src/gui/src/MainWindow.cpp
@@ -163,17 +163,8 @@ MainWindow::MainWindow(QSettings& settings, AppConfig& appConfig) :
 
     updateSSLFingerprint();
 
-    connect(toolbutton_show_fingerprint, &QToolButton::clicked, [this](bool checked)
-    {
-        m_fingerprint_expanded = !m_fingerprint_expanded;
-        if (m_fingerprint_expanded) {
-            frame_fingerprint_details->show();
-            toolbutton_show_fingerprint->setArrowType(Qt::ArrowType::UpArrow);
-        } else {
-            frame_fingerprint_details->hide();
-            toolbutton_show_fingerprint->setArrowType(Qt::ArrowType::DownArrow);
-        }
-    });
+    connect(toolbutton_show_fingerprint, SIGNAL(clicked()),
+            this, SLOT(toggleFingerprintDetails()));
 
     // resize window to smallest reasonable size
     resize(0, 0);
@@ -324,7 +315,9 @@ void MainWindow::setIcon(qBarrierState state)
 {
     if (m_pTrayIcon) {
         QIcon icon = QIcon::fromTheme(barrierIconNames[state], QIcon(barrierIconFiles[state]));
-#if defined(Q_OS_MAC)
+// QIcon::setIsMask() is Qt 5.6 and newer. Without it the tray icon simply is
+// not treated as a template image, so it does not adapt to the menu bar theme.
+#if defined(Q_OS_MAC) && QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)
         icon.setIsMask(true);
 #endif
         m_pTrayIcon->setIcon(icon);
@@ -1004,14 +997,24 @@ void MainWindow::serverDetected(const QString name)
     }
 }
 
+void MainWindow::toggleFingerprintDetails()
+{
+    m_fingerprint_expanded = !m_fingerprint_expanded;
+    if (m_fingerprint_expanded) {
+        frame_fingerprint_details->show();
+        toolbutton_show_fingerprint->setArrowType(Qt::UpArrow);
+    } else {
+        frame_fingerprint_details->hide();
+        toolbutton_show_fingerprint->setArrowType(Qt::DownArrow);
+    }
+}
+
 void MainWindow::updateSSLFingerprint()
 {
     if (m_AppConfig->getCryptoEnabled() && m_pSslCertificate == nullptr) {
         m_pSslCertificate = new SslCertificate(this);
-        connect(m_pSslCertificate, &SslCertificate::info, [&](QString info)
-        {
-            appendLogInfo(info);
-        });
+        connect(m_pSslCertificate, SIGNAL(info(QString)),
+                this, SLOT(appendLogInfo(QString)));
         m_pSslCertificate->generateCertificate();
     }
 
diff --git a/src/gui/src/MainWindow.h b/src/gui/src/MainWindow.h
index 0c582c9f..0118143e 100644
--- a/src/gui/src/MainWindow.h
+++ b/src/gui/src/MainWindow.h
@@ -206,6 +206,7 @@ private slots:
     void on_m_pComboServerList_currentIndexChanged(QString );
     void on_m_pButtonReload_clicked();
     void installBonjour();
+    void toggleFingerprintDetails();
 
 };
 
diff --git a/src/gui/src/ZeroconfServer.cpp b/src/gui/src/ZeroconfServer.cpp
index 40b97a50..e52373c0 100644
--- a/src/gui/src/ZeroconfServer.cpp
+++ b/src/gui/src/ZeroconfServer.cpp
@@ -25,7 +25,7 @@ ZeroconfServer::ZeroconfServer(QObject* parent) :
 {
 }
 
-void ZeroconfServer::incomingConnection(qintptr socketDescriptor)
+void ZeroconfServer::incomingConnection(BarrierSocketDescriptor socketDescriptor)
 {
     ZeroconfThread* thread = new ZeroconfThread(socketDescriptor, this);
     connect(thread, SIGNAL(finished()), thread, SLOT(deleteLater()));
diff --git a/src/gui/src/ZeroconfServer.h b/src/gui/src/ZeroconfServer.h
index 2e906d9a..90cdef6b 100644
--- a/src/gui/src/ZeroconfServer.h
+++ b/src/gui/src/ZeroconfServer.h
@@ -22,6 +22,14 @@
 
 class ZeroconfRegister;
 
+// QTcpServer::incomingConnection() takes an int in Qt4 and a qintptr in Qt5.
+// A plain typedef rather than an alias declaration, so that Qt4's moc can skip it.
+#if QT_VERSION >= QT_VERSION_CHECK(5, 0, 0)
+typedef qintptr BarrierSocketDescriptor;
+#else
+typedef int BarrierSocketDescriptor;
+#endif
+
 class ZeroconfServer : public QTcpServer
 {
     Q_OBJECT
@@ -30,7 +38,7 @@ public:
     ZeroconfServer(QObject* parent = 0);
 
 protected:
-    void incomingConnection(qintptr socketDescriptor) override;
+    void incomingConnection(BarrierSocketDescriptor socketDescriptor) override;
 
 private:
     QStringList fortunes;
diff --git a/src/gui/src/main.cpp b/src/gui/src/main.cpp
index 776b44de..92a0e936 100644
--- a/src/gui/src/main.cpp
+++ b/src/gui/src/main.cpp
@@ -97,11 +97,14 @@ int main(int argc, char* argv[])
 
 	QApplication::setQuitOnLastWindowClosed(false);
 
+#if QT_VERSION >= QT_VERSION_CHECK(5, 0, 0)
+    // Qt4 predates Wayland entirely, so there is nothing to warn about there.
     if (QGuiApplication::platformName() == "wayland") {
         QMessageBox::warning(
         NULL, "Barrier",
         "You are using wayland session, which is currently not fully supported by Barrier.");
     }
+#endif
 
 	QSettings settings;
 	AppConfig appConfig (&settings);
