From 946f51467fec8c90f7b85ae1d943f672f2c3d9bc Mon Sep 17 00:00:00 2001
From: Vojtech Bubnik <bubnikv@gmail.com>
Date: Tue, 8 Sep 2020 13:33:43 +0200
Subject: [PATCH] WIP Standalone G-code viewer

---
 src/CMakeLists.txt           |  42 ++++++++----
 src/PrusaSlicer.cpp          |  11 ++-
 src/slic3r/CMakeLists.txt    |   2 +
 src/slic3r/GUI/MainFrame.cpp |  25 ++-----
 src/slic3r/Utils/Process.cpp | 125 +++++++++++++++++++++++++++++++++++
 src/slic3r/Utils/Process.hpp |  19 ++++++
 src/slic3r/Utils/Thread.hpp  |   6 +-
 7 files changed, 192 insertions(+), 38 deletions(-)
 create mode 100644 src/slic3r/Utils/Process.cpp
 create mode 100644 src/slic3r/Utils/Process.hpp

diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 0b0b3c0ee..ca57ca553 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -106,9 +106,9 @@ if (MINGW)
     set_target_properties(PrusaSlicer PROPERTIES PREFIX "")
 endif (MINGW)
 
-if (NOT WIN32)
-    # Binary name on unix like systems (OSX, Linux)
-   set_target_properties(PrusaSlicer PROPERTIES OUTPUT_NAME "prusa-slicer")
+if (NOT WIN32 AND NOT APPLE)
+    # Binary name on unix like systems (Linux, Unix)
+    set_target_properties(PrusaSlicer PROPERTIES OUTPUT_NAME "prusa-slicer")
 endif ()
 
 target_link_libraries(PrusaSlicer libslic3r cereal)
@@ -209,20 +209,34 @@ if (WIN32)
     add_custom_target(PrusaSlicerDllsCopy ALL DEPENDS PrusaSlicer)
     prusaslicer_copy_dlls(PrusaSlicerDllsCopy)
     
-elseif (XCODE)
-    # Because of Debug/Release/etc. configurations (similar to MSVC) the slic3r binary is located in an extra level
-    add_custom_command(TARGET PrusaSlicer POST_BUILD
-        COMMAND ln -sfn "${SLIC3R_RESOURCES_DIR}" "${CMAKE_CURRENT_BINARY_DIR}/resources"
-        COMMENT "Symlinking the resources directory into the build tree"
-        VERBATIM
-    )
 else ()
+    if (APPLE)
+        # On OSX, the name of the binary matches the name of the Application.
+        add_custom_command(TARGET PrusaSlicer POST_BUILD
+            COMMAND ln -sf PrusaSlicer prusa-slicer
+            COMMAND ln -sf PrusaSlicer prusa-gcodeviewer
+            COMMAND ln -sf PrusaSlicer PrusaGCodeViewer
+            WORKING_DIRECTORY "$<TARGET_FILE_DIR:PrusaSlicer>"
+            COMMENT "Symlinking the G-code viewer to PrusaSlicer, symlinking to prusa-slicer and prusa-gcodeviewer"
+            VERBATIM)
+    else ()
+        add_custom_command(TARGET PrusaSlicer POST_BUILD
+            COMMAND ln -sf prusa-slicer prusa-gcodeviewer
+            WORKING_DIRECTORY "$<TARGET_FILE_DIR:PrusaSlicer>"
+            COMMENT "Symlinking the G-code viewer to PrusaSlicer"
+            VERBATIM)
+    endif ()
+    if (XCODE)
+        # Because of Debug/Release/etc. configurations (similar to MSVC) the slic3r binary is located in an extra level
+        set(BIN_RESOURCES_DIR "${CMAKE_CURRENT_BINARY_DIR}/resources")
+    else ()
+        set(BIN_RESOURCES_DIR "${CMAKE_CURRENT_BINARY_DIR}/../resources")
+    endif ()
     add_custom_command(TARGET PrusaSlicer POST_BUILD
-        COMMAND ln -sfn "${SLIC3R_RESOURCES_DIR}" "${CMAKE_CURRENT_BINARY_DIR}/../resources"
+        COMMAND ln -sfn "${SLIC3R_RESOURCES_DIR}" "${BIN_RESOURCES_DIR}"
         COMMENT "Symlinking the resources directory into the build tree"
-        VERBATIM
-    )
-endif()
+        VERBATIM)
+endif ()
 
 # Slic3r binary install target
 if (WIN32)
diff --git a/src/PrusaSlicer.cpp b/src/PrusaSlicer.cpp
index 2962f0cdf..94996dc92 100644
--- a/src/PrusaSlicer.cpp
+++ b/src/PrusaSlicer.cpp
@@ -22,6 +22,7 @@
 #include <cstring>
 #include <iostream>
 #include <math.h>
+#include <boost/algorithm/string/predicate.hpp>
 #include <boost/filesystem.hpp>
 #include <boost/nowide/args.hpp>
 #include <boost/nowide/cenv.hpp>
@@ -101,8 +102,14 @@ int CLI::run(int argc, char **argv)
         std::find(m_transforms.begin(), m_transforms.end(), "cut") == m_transforms.end() &&
         std::find(m_transforms.begin(), m_transforms.end(), "cut_x") == m_transforms.end() &&
         std::find(m_transforms.begin(), m_transforms.end(), "cut_y") == m_transforms.end();
-    bool 							start_as_gcodeviewer = false;
-    
+    bool 							start_as_gcodeviewer =
+#ifdef _WIN32
+            false;
+#else
+            // On Unix systems, the prusa-slicer binary may be symlinked to give the application a different meaning.
+            boost::algorithm::iends_with(boost::filesystem::path(argv[0]).filename().string(), "gcodeviewer");
+#endif // _WIN32
+
     const std::vector<std::string> &load_configs		= m_config.option<ConfigOptionStrings>("load", true)->values;
 
     // load config files supplied via --load
diff --git a/src/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt
index 5681ed66d..1c3007810 100644
--- a/src/slic3r/CMakeLists.txt
+++ b/src/slic3r/CMakeLists.txt
@@ -195,6 +195,8 @@ set(SLIC3R_GUI_SOURCES
     Utils/Bonjour.hpp
     Utils/PresetUpdater.cpp
     Utils/PresetUpdater.hpp
+    Utils/Process.cpp
+    Utils/Process.hpp
     Utils/Profile.hpp
     Utils/UndoRedo.cpp
     Utils/UndoRedo.hpp
diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp
index f6fd939e2..f4d7f03ec 100644
--- a/src/slic3r/GUI/MainFrame.cpp
+++ b/src/slic3r/GUI/MainFrame.cpp
@@ -9,7 +9,6 @@
 #include <wx/tooltip.h>
 //#include <wx/glcanvas.h>
 #include <wx/filename.h>
-#include <wx/stdpaths.h>
 #include <wx/debug.h>
 
 #include <boost/algorithm/string/predicate.hpp>
@@ -31,6 +30,7 @@
 #include "I18N.hpp"
 #include "GLCanvas3D.hpp"
 #include "Plater.hpp"
+#include "../Utils/Process.hpp"
 
 #include <fstream>
 #include "GUI_App.hpp"
@@ -40,12 +40,6 @@
 #include <shlobj.h>
 #endif // _WIN32
 
-// For starting another PrusaSlicer instance on OSX.
-// Fails to compile on Windows on the build server.
-#ifdef __APPLE__
-    #include <boost/process/spawn.hpp>
-#endif
-
 namespace Slic3r {
 namespace GUI {
 
@@ -1054,8 +1048,8 @@ void MainFrame::init_menubar()
         append_menu_item(fileMenu, wxID_ANY, _L("&Repair STL file") + dots, _L("Automatically repair an STL file"),
             [this](wxCommandEvent&) { repair_stl(); }, "wrench", nullptr,
             [this]() { return true; }, this);
-#if ENABLE_GCODE_VIEWER
         fileMenu->AppendSeparator();
+#if ENABLE_GCODE_VIEWER
         append_menu_item(fileMenu, wxID_ANY, _L("&G-code preview"), _L("Switch to G-code preview mode"),
             [this](wxCommandEvent&) {
                 if (m_plater->model().objects.empty() ||
@@ -1064,6 +1058,8 @@ void MainFrame::init_menubar()
                     set_mode(EMode::GCodeViewer);
             }, "", nullptr);
 #endif // ENABLE_GCODE_VIEWER
+        append_menu_item(fileMenu, wxID_ANY, _L("&G-code preview") + dots, _L("Open G-code viewer"),
+            [this](wxCommandEvent&) { start_new_gcodeviewer_open_file(this); }, "", nullptr);
         fileMenu->AppendSeparator();
         append_menu_item(fileMenu, wxID_EXIT, _L("&Quit"), wxString::Format(_L("Quit %s"), SLIC3R_APP_NAME),
             [this](wxCommandEvent&) { Close(false); });
@@ -1180,20 +1176,11 @@ void MainFrame::init_menubar()
 
         windowMenu->AppendSeparator();
         append_menu_item(windowMenu, wxID_ANY, _L("Print &Host Upload Queue") + "\tCtrl+J", _L("Display the Print Host Upload Queue window"),
-            [this](wxCommandEvent&) { m_printhost_queue_dlg->Show(); }, "upload_queue", nullptr,
-            [this]() {return true; }, this);
+            [this](wxCommandEvent&) { m_printhost_queue_dlg->Show(); }, "upload_queue", nullptr, [this]() {return true; }, this);
         
         windowMenu->AppendSeparator();
         append_menu_item(windowMenu, wxID_ANY, _(L("Open new instance")) + "\tCtrl+I", _(L("Open a new PrusaSlicer instance")),
-                         [this](wxCommandEvent&) {
-                             wxString path = wxStandardPaths::Get().GetExecutablePath();
-#ifdef __APPLE__
-                             boost::process::spawn((const char*)path.c_str());
-#else
-                             wxExecute(wxStandardPaths::Get().GetExecutablePath(), wxEXEC_ASYNC | wxEXEC_HIDE_CONSOLE | wxEXEC_MAKE_GROUP_LEADER);
-#endif
-                         }, "upload_queue", nullptr,
-                         [this]() {return true; }, this);
+			[this](wxCommandEvent&) { start_new_slicer(); }, "", nullptr);
     }
 
     // View menu
diff --git a/src/slic3r/Utils/Process.cpp b/src/slic3r/Utils/Process.cpp
new file mode 100644
index 000000000..2301cd250
--- /dev/null
+++ b/src/slic3r/Utils/Process.cpp
@@ -0,0 +1,125 @@
+#include "Process.hpp"
+
+#include <libslic3r/AppConfig.hpp>
+
+#include "../GUI/GUI.hpp"
+// for file_wildcards()
+#include "../GUI/GUI_App.hpp"
+// localization
+#include "../GUI/I18N.hpp"
+
+#include <iostream>
+#include <fstream>
+
+#include <boost/filesystem.hpp>
+#include <boost/log/trivial.hpp>
+
+// For starting another PrusaSlicer instance on OSX.
+// Fails to compile on Windows on the build server.
+#ifdef __APPLE__
+    #include <boost/process/spawn.hpp>
+#endif
+
+#include <wx/stdpaths.h>
+
+namespace Slic3r {
+namespace GUI {
+
+enum class NewSlicerInstanceType {
+	Slicer,
+	GCodeViewer
+};
+
+// Start a new Slicer process instance either in a Slicer mode or in a G-code mode.
+// Optionally load a 3MF, STL or a G-code on start.
+static void start_new_slicer_or_gcodeviewer(const NewSlicerInstanceType instance_type, const wxString *path_to_open)
+{
+#ifdef _WIN32
+	wxString path;
+	wxFileName::SplitPath(wxStandardPaths::Get().GetExecutablePath(), &path, nullptr, nullptr, wxPATH_NATIVE);
+	path += "\\";
+	path += (instance_type == NewSlicerInstanceType::Slicer) ? "prusa-slicer.exe" : "prusa-gcodeviewer.exe";
+	std::vector<const wchar_t*> args;
+	args.reserve(3);
+	args.emplace_back(path.wc_str());
+	if (path_to_open != nullptr)
+		args.emplace_back(path_to_open->wc_str());
+	args.emplace_back(nullptr);
+	BOOST_LOG_TRIVIAL(info) << "Trying to spawn a new slicer \"" << to_u8(path) << "\"";
+	if (wxExecute(const_cast<wchar_t**>(args.data()), wxEXEC_ASYNC | wxEXEC_HIDE_CONSOLE | wxEXEC_MAKE_GROUP_LEADER) <= 0)
+		BOOST_LOG_TRIVIAL(error) << "Failed to spawn a new slicer \"" << to_u8(path);
+#else 
+	// Own executable path.
+    boost::filesystem::path bin_path = into_path(wxStandardPaths::Get().GetExecutablePath());
+	#if defined(__APPLE__)
+	{
+		bin_path =	bin_path.parent_path() / ((instance_type == NewSlicerInstanceType::Slicer) ? "PrusaSlicer" : "PrusaGCodeViewer");
+		// On Apple the wxExecute fails, thus we use boost::process instead.
+		BOOST_LOG_TRIVIAL(info) << "Trying to spawn a new slicer \"" << bin_path.string() << "\"";
+		try {
+	    	path_to_open ? boost::process::spawn(bin_path, into_u8(*path_to_open)) : boost::process::spawn(bin_path, boost::process::args());
+	    } catch (const std::exception &ex) {
+			BOOST_LOG_TRIVIAL(error) << "Failed to spawn a new slicer \"" << bin_path.string() << "\": " << ex.what();
+	    }
+	}
+	#else // Linux or Unix
+	{
+		std::vector<const char*> args;
+	    args.reserve(3);
+		#ifdef __linux
+		static const char *gcodeviewer_param = "--gcodeviewer";
+	    {
+			// If executed by an AppImage, start the AppImage, not the main process.
+			// see https://docs.appimage.org/packaging-guide/environment-variables.html#id2
+			const char *appimage_binary = std::getenv("APPIMAGE");
+			if (appimage_binary) {
+				args.emplace_back(appimage_binary);
+				if (instance_type == NewSlicerInstanceType::GCodeViewer)
+					args.emplace_back(gcodeviewer_param);
+			}
+		}
+		#endif // __linux
+		std::string my_path;
+		if (args.empty()) {
+			// Binary path was not set to the AppImage in the Linux specific block above, call the application directly.
+			my_path = (bin_path.parent_path() / ((instance_type == NewSlicerInstanceType::Slicer) ? "prusa-slicer" : "prusa-gcodeviewer")).string();
+			args.emplace_back(my_path.c_str());
+		}
+	    std::string to_open;
+	    if (path_to_open) {
+	    	to_open = into_u8(*path_to_open);
+	    	args.emplace_back(to_open.c_str());
+	    }
+	    args.emplace_back(nullptr);
+		BOOST_LOG_TRIVIAL(info) << "Trying to spawn a new slicer \"" << args[0] << "\"";
+	    if (wxExecute(const_cast<char**>(args.data()), wxEXEC_ASYNC | wxEXEC_HIDE_CONSOLE | wxEXEC_MAKE_GROUP_LEADER) <= 0)
+	    	BOOST_LOG_TRIVIAL(error) << "Failed to spawn a new slicer \"" << args[0];
+	}
+	#endif // Linux or Unix
+#endif // Win32
+}
+
+void start_new_slicer(const wxString *path_to_open)
+{
+	start_new_slicer_or_gcodeviewer(NewSlicerInstanceType::Slicer, path_to_open);
+}
+
+void start_new_gcodeviewer(const wxString *path_to_open)
+{
+	start_new_slicer_or_gcodeviewer(NewSlicerInstanceType::GCodeViewer, path_to_open);
+}
+
+void start_new_gcodeviewer_open_file(wxWindow *parent)
+{
+    wxFileDialog dialog(parent ? parent : wxGetApp().GetTopWindow(),
+        _L("Open G-code file:"),
+        from_u8(wxGetApp().app_config->get_last_dir()), wxString(),
+        file_wildcards(FT_GCODE), wxFD_OPEN | wxFD_FILE_MUST_EXIST);
+    if (dialog.ShowModal() == wxID_OK) {
+        wxString path = dialog.GetPath();
+		start_new_gcodeviewer(&path);
+    }
+}
+
+} // namespace GUI
+} // namespace Slic3r
diff --git a/src/slic3r/Utils/Process.hpp b/src/slic3r/Utils/Process.hpp
new file mode 100644
index 000000000..c6acaa643
--- /dev/null
+++ b/src/slic3r/Utils/Process.hpp
@@ -0,0 +1,19 @@
+#ifndef GUI_PROCESS_HPP
+#define GUI_PROCESS_HPP
+
+class wxWindow;
+
+namespace Slic3r {
+namespace GUI {
+
+// Start a new slicer instance, optionally with a file to open.
+void start_new_slicer(const wxString *path_to_open = nullptr);
+// Start a new G-code viewer instance, optionally with a file to open.
+void start_new_gcodeviewer(const wxString *path_to_open = nullptr);
+// Open a file dialog, ask the user to select a new G-code to open, start a new G-code viewer.
+void start_new_gcodeviewer_open_file(wxWindow *parent = nullptr);
+
+} // namespace GUI
+} // namespace Slic3r
+
+#endif // GUI_PROCESS_HPP
diff --git a/src/slic3r/Utils/Thread.hpp b/src/slic3r/Utils/Thread.hpp
index e9c76d2ab..194971c9e 100644
--- a/src/slic3r/Utils/Thread.hpp
+++ b/src/slic3r/Utils/Thread.hpp
@@ -1,5 +1,5 @@
-#ifndef THREAD_HPP
-#define THREAD_HPP
+#ifndef GUI_THREAD_HPP
+#define GUI_THREAD_HPP
 
 #include <utility>
 #include <boost/thread.hpp>
@@ -25,4 +25,4 @@ template<class Fn> inline boost::thread create_thread(Fn &&fn)
 
 }
 
-#endif // THREAD_HPP
+#endif // GUI_THREAD_HPP