From 963849e18b4f1fe55406f82867bdb564b4122433 Mon Sep 17 00:00:00 2001
From: David Kocik <kocikdav@gmail.com>
Date: Thu, 6 May 2021 17:37:55 +0200
Subject: [PATCH] desktop integration functions and dialog

---
 src/slic3r/CMakeLists.txt                   |   2 +
 src/slic3r/GUI/ConfigWizard.cpp             |  31 +-
 src/slic3r/GUI/ConfigWizard.hpp             |   1 -
 src/slic3r/GUI/ConfigWizard_private.hpp     |   6 +-
 src/slic3r/GUI/DesktopIntegrationDialog.cpp | 439 ++++++++++++++++++++
 src/slic3r/GUI/DesktopIntegrationDialog.hpp |  39 ++
 src/slic3r/GUI/GUI_App.cpp                  |  19 +
 src/slic3r/GUI/GUI_App.hpp                  |   2 +
 src/slic3r/GUI/NotificationManager.hpp      |  16 +-
 9 files changed, 550 insertions(+), 5 deletions(-)
 create mode 100644 src/slic3r/GUI/DesktopIntegrationDialog.cpp
 create mode 100644 src/slic3r/GUI/DesktopIntegrationDialog.hpp

diff --git a/src/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt
index 4b3a1c6ca..7f4b87439 100644
--- a/src/slic3r/CMakeLists.txt
+++ b/src/slic3r/CMakeLists.txt
@@ -189,6 +189,8 @@ set(SLIC3R_GUI_SOURCES
     GUI/UnsavedChangesDialog.hpp
     GUI/ExtraRenderers.cpp
     GUI/ExtraRenderers.hpp
+    GUI/DesktopIntegrationDialog.cpp
+    GUI/DesktopIntegrationDialog.hpp
     Utils/Http.cpp
     Utils/Http.hpp
     Utils/FixModelByWin10.cpp
diff --git a/src/slic3r/GUI/ConfigWizard.cpp b/src/slic3r/GUI/ConfigWizard.cpp
index bd6986d3e..1d07b137e 100644
--- a/src/slic3r/GUI/ConfigWizard.cpp
+++ b/src/slic3r/GUI/ConfigWizard.cpp
@@ -26,14 +26,17 @@
 #include <wx/wupdlock.h>
 #include <wx/debug.h>
 
+#include "libslic3r/Platform.hpp"
 #include "libslic3r/Utils.hpp"
 #include "libslic3r/Config.hpp"
 #include "GUI.hpp"
 #include "GUI_App.hpp"
 #include "GUI_Utils.hpp"
 #include "GUI_ObjectManipulation.hpp"
+#include "DesktopIntegrationDialog.hpp"
 #include "slic3r/Config/Snapshot.hpp"
 #include "slic3r/Utils/PresetUpdater.hpp"
+#include "format.hpp"
 
 #if defined(__linux__) && defined(__WXGTK3__)
 #define wxLinux_gtk3 true
@@ -450,7 +453,6 @@ void ConfigWizardPage::append_spacer(int space)
     content->AddSpacer(space);
 }
 
-
 // Wizard pages
 
 PageWelcome::PageWelcome(ConfigWizard *parent)
@@ -469,9 +471,21 @@ PageWelcome::PageWelcome(ConfigWizard *parent)
     , cbox_reset(append(
         new wxCheckBox(this, wxID_ANY, _L("Remove user profiles (a snapshot will be taken beforehand)"))
     ))
+    , cbox_integrate(append(
+        new wxCheckBox(this, wxID_ANY, _L("Perform desktop integration (This will set shortcuts to PrusaSlicer to this Appimage executable)."))
+    ))
 {
     welcome_text->Hide();
     cbox_reset->Hide();
+#ifdef __linux__
+    if (!DesktopIntegrationDialog::is_integrated())
+        cbox_integrate->Show(true);
+    else
+        cbox_integrate->Hide();
+#else
+    cbox_integrate->Hide();
+#endif
+    
 }
 
 void PageWelcome::set_run_reason(ConfigWizard::RunReason run_reason)
@@ -479,6 +493,14 @@ void PageWelcome::set_run_reason(ConfigWizard::RunReason run_reason)
     const bool data_empty = run_reason == ConfigWizard::RR_DATA_EMPTY;
     welcome_text->Show(data_empty);
     cbox_reset->Show(!data_empty);
+#ifdef __linux__
+    if (!DesktopIntegrationDialog::is_integrated())
+        cbox_integrate->Show(true);
+    else
+        cbox_integrate->Hide();
+#else
+    cbox_integrate->Hide();
+#endif
 }
 
 
@@ -2373,6 +2395,12 @@ void ConfigWizard::priv::apply_config(AppConfig *app_config, PresetBundle *prese
         }
     }
 
+#ifdef __linux__
+    // Desktop integration on Linux
+    if (page_welcome->integrate_desktop()) 
+        DesktopIntegrationDialog::perform_desktop_integration();
+#endif
+
     // Decide whether to create snapshot based on run_reason and the reset profile checkbox
     bool snapshot = true;
     Snapshot::Reason snapshot_reason = Snapshot::SNAPSHOT_UPGRADE;
@@ -2490,7 +2518,6 @@ void ConfigWizard::priv::apply_config(AppConfig *app_config, PresetBundle *prese
     // Update the selections from the compatibilty.
     preset_bundle->export_selections(*app_config);
 }
-
 void ConfigWizard::priv::update_presets_in_config(const std::string& section, const std::string& alias_key, bool add)
 {
     const PresetAliases& aliases = section == AppConfig::SECTION_FILAMENTS ? aliases_fff : aliases_sla;
diff --git a/src/slic3r/GUI/ConfigWizard.hpp b/src/slic3r/GUI/ConfigWizard.hpp
index 942f4b4ce..86245836b 100644
--- a/src/slic3r/GUI/ConfigWizard.hpp
+++ b/src/slic3r/GUI/ConfigWizard.hpp
@@ -45,7 +45,6 @@ public:
     bool run(RunReason reason, StartPage start_page = SP_WELCOME);
 
     static const wxString& name(const bool from_menu = false);
-
 protected:
     void on_dpi_changed(const wxRect &suggested_rect) override ;
 
diff --git a/src/slic3r/GUI/ConfigWizard_private.hpp b/src/slic3r/GUI/ConfigWizard_private.hpp
index eee906ae7..4e3f1538e 100644
--- a/src/slic3r/GUI/ConfigWizard_private.hpp
+++ b/src/slic3r/GUI/ConfigWizard_private.hpp
@@ -227,10 +227,12 @@ struct PageWelcome: ConfigWizardPage
 {
     wxStaticText *welcome_text;
     wxCheckBox *cbox_reset;
+    wxCheckBox *cbox_integrate;
 
     PageWelcome(ConfigWizard *parent);
 
     bool reset_user_profile() const { return cbox_reset != nullptr ? cbox_reset->GetValue() : false; }
+    bool integrate_desktop() const { return cbox_integrate != nullptr ? cbox_integrate->GetValue() : false; }
 
     virtual void set_run_reason(ConfigWizard::RunReason run_reason) override;
 };
@@ -615,7 +617,9 @@ struct ConfigWizard::priv
     void apply_config(AppConfig *app_config, PresetBundle *preset_bundle, const PresetUpdater *updater);
     // #ys_FIXME_alise
     void update_presets_in_config(const std::string& section, const std::string& alias_key, bool add);
-
+#ifdef __linux__
+    void perform_desktop_integration() const;
+#endif
     bool check_fff_selected();        // Used to decide whether to display Filaments page
     bool check_sla_selected();        // Used to decide whether to display SLA Materials page
 
diff --git a/src/slic3r/GUI/DesktopIntegrationDialog.cpp b/src/slic3r/GUI/DesktopIntegrationDialog.cpp
new file mode 100644
index 000000000..25a5ab575
--- /dev/null
+++ b/src/slic3r/GUI/DesktopIntegrationDialog.cpp
@@ -0,0 +1,439 @@
+#ifdef __linux__
+#include "DesktopIntegrationDialog.hpp"
+#include "GUI_App.hpp"
+#include "format.hpp"
+#include "I18N.hpp"
+#include "NotificationManager.hpp"
+#include "libslic3r/AppConfig.hpp"
+#include "libslic3r/Utils.hpp"
+#include "libslic3r/Platform.hpp"
+
+namespace Slic3r {
+namespace GUI {
+
+namespace integrate_desktop_internal{
+// Disects path strings stored in system variable divided by ':' and adds into vector
+static void resolve_path_from_var(const std::string& var, std::vector<std::string>& paths)
+{
+    wxString wxdirs;
+    if (! wxGetEnv(boost::nowide::widen(var), &wxdirs) || wxdirs.empty() )
+        return;
+    std::string dirs = boost::nowide::narrow(wxdirs);
+    for (size_t i = dirs.find(':'); i != std::string::npos; i = dirs.find(':'))
+    {
+        paths.push_back(dirs.substr(0, i));
+        if (dirs.size() > i+1)
+            dirs = dirs.substr(i+1);
+    }
+    paths.push_back(dirs);
+}
+// Return true if directory in path p+dir_name exists
+static bool contains_path_dir(const std::string& p, const std::string& dir_name)
+{
+    if (p.empty() || dir_name.empty()) 
+       return false;
+    boost::filesystem::path path(p + (p[p.size()-1] == '/' ? "" : "/") + dir_name);
+    if (boost::filesystem::exists(path) && boost::filesystem::is_directory(path)) {
+        //BOOST_LOG_TRIVIAL(debug) << path.string() << " " << std::oct << boost::filesystem::status(path).permissions();
+        return true; //boost::filesystem::status(path).permissions() & boost::filesystem::owner_write;
+    } else
+        BOOST_LOG_TRIVIAL(debug) << path.string() << " doesnt exists";
+    return false;
+}
+// Creates directory in path if not exists yet
+static void create_dir(const boost::filesystem::path& path)
+{
+    if (boost::filesystem::exists(path))
+        return;
+    BOOST_LOG_TRIVIAL(debug)<< "creating " << path.string();
+    boost::system::error_code ec;
+    boost::filesystem::create_directory(path, ec);
+    if (ec)
+        BOOST_LOG_TRIVIAL(error)<< "create directory failed: " << ec.message();
+}
+// Starts at basic_path (excluded) and creates all directories in dir_path
+static void create_path(const std::string& basic_path, const std::string& dir_path)
+{
+    if (basic_path.empty() || dir_path.empty())
+       return;
+
+    boost::filesystem::path path(basic_path);
+    std::string dirs = dir_path;
+    for (size_t i = dirs.find('/'); i != std::string::npos; i = dirs.find('/'))
+    {
+        std::string dir = dirs.substr(0, i);
+        path = boost::filesystem::path(path.string() +"/"+ dir);
+        create_dir(path);
+        dirs = dirs.substr(i+1);
+    }
+    path = boost::filesystem::path(path.string() +"/"+ dirs);
+    create_dir(path);
+}
+// Calls our internal copy_file function to copy file at icon_path to dest_path
+static bool copy_icon(const std::string& icon_path, const std::string& dest_path)
+{
+    BOOST_LOG_TRIVIAL(debug) <<"icon from "<< icon_path;
+    BOOST_LOG_TRIVIAL(debug) <<"icon to "<< dest_path;
+    std::string error_message;
+    auto cfr = copy_file(icon_path, dest_path, error_message, false);
+    if (cfr) {
+        BOOST_LOG_TRIVIAL(debug) << "Copy icon fail(" << cfr << "): " << error_message;
+        return false;
+    }
+    BOOST_LOG_TRIVIAL(debug) << "Copy icon success.";
+    return true;
+}
+// Creates new file filled with data.
+static bool create_desktop_file(const std::string& path, const std::string& data)
+{
+    BOOST_LOG_TRIVIAL(debug) <<".desktop to "<< path;
+    std::ofstream output(path);
+    output << data;
+    struct stat buffer;
+    if (stat(path.c_str(), &buffer) == 0)
+    {
+        BOOST_LOG_TRIVIAL(debug) << "Desktop file created.";
+        return true;
+    }
+    BOOST_LOG_TRIVIAL(debug) << "Desktop file NOT created.";
+    return false;
+}
+} // namespace integratec_desktop_internal
+
+// methods that actually do / undo desktop integration. Static to be accesible from anywhere.
+bool DesktopIntegrationDialog::is_integrated()
+{
+	const char *appimage_env = std::getenv("APPIMAGE");
+    if (!appimage_env) 
+        return false;
+ 
+    const AppConfig *app_config = wxGetApp().app_config;
+    std::string path(app_config->get("desktop_integration_app_path"));
+    BOOST_LOG_TRIVIAL(debug) << "Desktop integration desktop file path: " << path;
+
+    if (path.empty())
+        return false;
+
+    // confirmation that PrusaSlicer.desktop exists
+    struct stat buffer;   
+    return (stat (path.c_str(), &buffer) == 0);
+}
+bool DesktopIntegrationDialog::integration_possible()
+{
+
+	const char *appimage_env = std::getenv("APPIMAGE");
+    if (!appimage_env)
+        return false;
+    return true;
+}
+void DesktopIntegrationDialog::perform_desktop_integration()
+{
+	BOOST_LOG_TRIVIAL(debug) << "performing desktop integration";
+
+    // Path to appimage
+    const char *appimage_env = std::getenv("APPIMAGE");
+    std::string appimage_path;
+    if (appimage_env) {
+        try {
+            appimage_path = boost::filesystem::canonical(boost::filesystem::path(appimage_env)).string();
+        } catch (std::exception &) {            
+        }
+    } else {
+        // not appimage - not performing
+        BOOST_LOG_TRIVIAL(error) << "Performing desktop integration failed - not Appimage executable.";
+        wxGetApp().plater()->get_notification_manager()->push_notification(NotificationType::DesktopIntegrationFail);
+        return;
+    }
+
+    // Find directories icons and applications
+    // $XDG_DATA_HOME defines the base directory relative to which user specific data files should be stored. 
+    // If $XDG_DATA_HOME is either not set or empty, a default equal to $HOME/.local/share should be used. 
+    // $XDG_DATA_DIRS defines the preference-ordered set of base directories to search for data files in addition to the $XDG_DATA_HOME base directory.
+    // The directories in $XDG_DATA_DIRS should be seperated with a colon ':'.
+    // If $XDG_DATA_DIRS is either not set or empty, a value equal to /usr/local/share/:/usr/share/ should be used. 
+    std::vector<std::string>target_candidates;
+    integrate_desktop_internal::resolve_path_from_var("XDG_DATA_HOME", target_candidates);
+    integrate_desktop_internal::resolve_path_from_var("XDG_DATA_DIRS", target_candidates);
+
+    AppConfig *app_config = wxGetApp().app_config;
+    // suffix string to create different desktop file for alpha, beta.
+    
+    std::string version_suffix;
+    std::string name_suffix;
+    std::string version(SLIC3R_VERSION);
+    if (version.find("alpha") != std::string::npos)
+    {
+        version_suffix = "-alpha";
+        name_suffix = " - alpha";
+    }else if (version.find("beta") != std::string::npos)
+    {
+        version_suffix = "-beta";
+        name_suffix = " - beta";
+    }
+
+    // theme path to icon destination    
+    std::string icon_theme_path;
+    std::string icon_theme_dirs;
+
+    if (platform_flavor() == PlatformFlavor::LinuxOnChromium) {
+        icon_theme_path = "hicolor/96x96/apps/";
+        icon_theme_dirs = "/hicolor/96x96/apps";
+    }
+    
+    
+    std::string target_dir_icons;
+    std::string target_dir_desktop;
+    
+    // slicer icon
+    // iterate thru target_candidates to find icons folder
+    for (size_t i = 0; i < target_candidates.size(); ++i) {
+        // Copy icon PrusaSlicer.png from resources_dir()/icons to target_dir_icons/icons/
+        if (integrate_desktop_internal::contains_path_dir(target_candidates[i], "icons")) {
+            target_dir_icons = target_candidates[i];
+            std::string icon_path = GUI::format("%1%/icons/PrusaSlicer.png",resources_dir());
+            std::string dest_path = GUI::format("%1%/icons/%2%PrusaSlicer%3%.png", target_dir_icons, icon_theme_path, version_suffix);
+            if (integrate_desktop_internal::copy_icon(icon_path, dest_path))
+                break; // success
+            else
+                target_dir_icons.clear(); // copying failed
+            // if all failed - try creating default home folder
+            if (i == target_candidates.size() - 1) {
+                // create $HOME/.local/share
+                integrate_desktop_internal::create_path(boost::nowide::narrow(wxFileName::GetHomeDir()), ".local/share/icons" + icon_theme_dirs);
+                // copy icon
+                target_dir_icons = GUI::format("%1%/.local/share",wxFileName::GetHomeDir());
+                std::string icon_path = GUI::format("%1%/icons/PrusaSlicer.png",resources_dir());
+                std::string dest_path = GUI::format("%1%/icons/%2%PrusaSlicer%3%.png", target_dir_icons, icon_theme_path, version_suffix);
+                if (!integrate_desktop_internal::contains_path_dir(target_dir_icons, "icons") 
+                    || !integrate_desktop_internal::copy_icon(icon_path, dest_path)) {
+                	// every attempt failed - icon wont be present
+                    target_dir_icons.clear(); 
+                }
+            }
+        }
+    }
+    if(target_dir_icons.empty()) {
+        BOOST_LOG_TRIVIAL(error) << "Copying PrusaSlicer icon to icons directory failed.";
+    } else 
+    	// save path to icon
+        app_config->set("desktop_integration_icon_slicer_path", GUI::format("%1%/icons/%2%PrusaSlicer%3%.png", target_dir_icons, icon_theme_path, version_suffix));
+
+    // desktop file
+    // iterate thru target_candidates to find applications folder
+    for (size_t i = 0; i < target_candidates.size(); ++i)
+    {
+        if (integrate_desktop_internal::contains_path_dir(target_candidates[i], "applications")) {
+            target_dir_desktop = target_candidates[i];
+            // Write slicer desktop file
+            std::string desktop_file = GUI::format(
+                "[Desktop Entry]\n"
+                "Name=PrusaSlicer%1%\n"
+                "GenericName=3D Printing Software\n"
+                "Icon=PrusaSlicer%2%\n"
+                "Exec=%3% %%F\n"
+                "Terminal=false\n"
+                "Type=Application\n"
+                "MimeType=model/stl;application/vnd.ms-3mfdocument;application/prs.wavefront-obj;application/x-amf;\n"
+                "Categories=Graphics;3DGraphics;Engineering;\n"
+                "Keywords=3D;Printing;Slicer;slice;3D;printer;convert;gcode;stl;obj;amf;SLA\n"
+                "StartupNotify=false\n"
+                "StartupWMClass=prusa-slicer", name_suffix, version_suffix, appimage_path);
+
+            std::string path = GUI::format("%1%/applications/PrusaSlicer%2%.desktop", target_dir_desktop, version_suffix);
+            if (integrate_desktop_internal::create_desktop_file(path, desktop_file)){
+                BOOST_LOG_TRIVIAL(debug) << "PrusaSlicer.desktop file installation success.";
+                break;
+            } else {
+            	// write failed - try another path
+                BOOST_LOG_TRIVIAL(error) << "PrusaSlicer.desktop file installation failed.";
+                target_dir_desktop.clear(); 
+            }
+            // if all failed - try creating default home folder
+            if (i == target_candidates.size() - 1) {
+                // create $HOME/.local/share
+                integrate_desktop_internal::create_path(boost::nowide::narrow(wxFileName::GetHomeDir()), ".local/share/applications");
+                // create desktop file
+                target_dir_desktop = GUI::format("%1%/.local/share",wxFileName::GetHomeDir());
+                std::string path = GUI::format("%1%/applications/PrusaSlicer%2%.desktop", target_dir_desktop, version_suffix);
+                if (integrate_desktop_internal::contains_path_dir(target_dir_desktop, "applications")) {
+                    if (!integrate_desktop_internal::create_desktop_file(path, desktop_file)) {    
+                        // Desktop file not written - end desktop integration
+                        BOOST_LOG_TRIVIAL(error) << "Performing desktop integration failed - could not create desktop file";
+                        return;
+                    }
+                } else {
+                	// Desktop file not written - end desktop integration
+                    BOOST_LOG_TRIVIAL(error) << "Performing desktop integration failed - could not find applications directory";
+                    return;
+                }
+            }
+        }
+    }
+    if(target_dir_desktop.empty()) {
+    	// Desktop file not written - end desktop integration
+        BOOST_LOG_TRIVIAL(error) << "Performing desktop integration failed - could not find applications directory";
+        wxGetApp().plater()->get_notification_manager()->push_notification(NotificationType::DesktopIntegrationFail);
+        return;
+    }
+    // save path to desktop file
+    app_config->set("desktop_integration_app_path", GUI::format("%1%/applications/PrusaSlicer%2%.desktop", target_dir_desktop, version_suffix));
+
+    // Repeat for Gcode viewer - use same paths as for slicer files
+    // Icon
+    if (!target_dir_icons.empty())
+    {
+    	std::string icon_path = GUI::format("%1%/icons/PrusaSlicer-gcodeviewer_192px.png",resources_dir());
+	    std::string dest_path = GUI::format("%1%/icons/%2%PrusaSlicer-gcodeviewer%3%.png", target_dir_icons, icon_theme_path, version_suffix);
+	    if (integrate_desktop_internal::copy_icon(icon_path, dest_path))
+	    	// save path to icon
+	        app_config->set("desktop_integration_icon_viewer_path", dest_path);
+	    else
+	        BOOST_LOG_TRIVIAL(error) << "Copying Gcode Viewer icon to icons directory failed.";
+    }
+
+    // Desktop file
+    std::string desktop_file = GUI::format(
+        "[Desktop Entry]\n"
+        "Name=Prusa Gcode Viewer%1%\n"
+        "GenericName=3D Printing Software\n"
+        "Icon=PrusaSlicer-gcodeviewer%2%\n"
+        "Exec=%3% --gcodeviwer %%F\n"
+        "Terminal=false\n"
+        "Type=Application\n"
+        "MimeType=text/x.gcode;\n"
+        "Categories=Graphics;3DGraphics;\n"
+        "Keywords=3D;Printing;Slicer;\n"
+        "StartupNotify=false", name_suffix, version_suffix, appimage_path);
+
+    std::string desktop_path = GUI::format("%1%/applications/PrusaSlicerGcodeViewer%2%.desktop", target_dir_desktop, version_suffix);
+    if (integrate_desktop_internal::create_desktop_file(desktop_path, desktop_file))
+    	// save path to desktop file
+        app_config->set("desktop_integration_app_viewer_path", desktop_path);
+    else {
+        BOOST_LOG_TRIVIAL(error) << "Performing desktop integration failed - could create gcode viewer desktop file";
+         wxGetApp().plater()->get_notification_manager()->push_notification(NotificationType::DesktopIntegrationFail);
+    }
+    wxGetApp().plater()->get_notification_manager()->push_notification(NotificationType::DesktopIntegrationSuccess);
+}
+void DesktopIntegrationDialog::undo_desktop_intgration()
+{
+	const char *appimage_env = std::getenv("APPIMAGE");
+    if (!appimage_env) {
+        BOOST_LOG_TRIVIAL(error) << "Undo desktop integration failed - not Appimage executable.";
+    	wxGetApp().plater()->get_notification_manager()->push_notification(NotificationType::UndoDesktopIntegrationFail);
+        return;
+    }
+    const AppConfig *app_config = wxGetApp().app_config;
+    // slicer .desktop
+    std::string path = std::string(app_config->get("desktop_integration_app_path"));
+    if (!path.empty()) {
+    	BOOST_LOG_TRIVIAL(debug) << "removing " << path;
+        std::remove(path.c_str());  
+    }
+    // slicer icon
+    path = std::string(app_config->get("desktop_integration_icon_slicer_path"));
+    if (!path.empty()) {
+    	BOOST_LOG_TRIVIAL(debug) << "removing " << path;
+        std::remove(path.c_str());
+    }
+    // gcode viwer .desktop
+    path = std::string(app_config->get("desktop_integration_app_viewer_path"));
+    if (!path.empty()) {
+    	BOOST_LOG_TRIVIAL(debug) << "removing " << path;
+        std::remove(path.c_str());
+    }
+     // gcode viewer icon
+    path = std::string(app_config->get("desktop_integration_icon_viewer_path"));
+    if (!path.empty()) {
+    	BOOST_LOG_TRIVIAL(debug) << "removing " << path;
+        std::remove(path.c_str());
+    }
+    wxGetApp().plater()->get_notification_manager()->push_notification(NotificationType::UndoDesktopIntegrationSuccess);
+}
+
+DesktopIntegrationDialog::DesktopIntegrationDialog(wxWindow *parent)
+: wxDialog(parent, wxID_ANY, _(L("Desktop Integration")), wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE|wxRESIZE_BORDER)
+{
+	bool can_undo = DesktopIntegrationDialog::is_integrated();
+
+	wxBoxSizer *vbox = new wxBoxSizer(wxVERTICAL);
+
+
+	wxString text = _L("Desktop Integration sets this binary to be searchable by the system.\n\nPress \"Perform\" to proceed.");
+	if (can_undo)
+		text += "\nPress \"Undo\" to remove previous integration.";
+
+    vbox->Add(
+        new wxStaticText( this, wxID_ANY, text),
+        //	, wxDefaultPosition, wxSize(100,50), wxTE_MULTILINE),
+        1,            // make vertically stretchable
+        wxEXPAND |    // make horizontally stretchable
+        wxALL,        //   and make border all around
+        10 );         // set border width to 10
+	
+
+	wxBoxSizer *btn_szr = new wxBoxSizer(wxHORIZONTAL);
+	wxButton *btn_perform = new wxButton(this, wxID_ANY, _L("Perform"));
+	btn_szr->Add(btn_perform, 0, wxALL, 10);
+
+	btn_perform->Bind(wxEVT_BUTTON, [this](wxCommandEvent &) { DesktopIntegrationDialog::perform_desktop_integration(); EndModal(wxID_ANY); });
+	
+	if (can_undo){
+		wxButton *btn_undo = new wxButton(this, wxID_ANY, _L("Undo"));
+		btn_szr->Add(btn_undo, 0, wxALL, 10);
+		btn_undo->Bind(wxEVT_BUTTON, [this](wxCommandEvent &) { DesktopIntegrationDialog::undo_desktop_intgration(); EndModal(wxID_ANY); });
+	}
+	wxButton *btn_cancel = new wxButton(this, wxID_ANY, _L("Cancel"));
+	btn_szr->Add(btn_cancel, 0, wxALL, 10);
+	btn_cancel->Bind(wxEVT_BUTTON, [this](wxCommandEvent &) { EndModal(wxID_ANY); });
+
+	vbox->Add(btn_szr, 0, wxALIGN_CENTER);
+
+    SetSizerAndFit(vbox);
+
+
+/*
+    //boldfont.SetWeight(wxFONTWEIGHT_BOLD);
+
+    //this->SetFont(wxGetApp().normal_font());
+
+	auto *topsizer = new wxBoxSizer(wxHORIZONTAL);
+	auto *rightsizer = new wxBoxSizer(wxVERTICAL);
+
+	auto *headtext = new wxStaticText(this, wxID_ANY, headline);
+	headtext->SetFont(boldfont);
+    headtext->Wrap(50*wxGetApp().em_unit());
+	rightsizer->Add(headtext);
+	rightsizer->AddSpacer(VERT_SPACING);
+
+	rightsizer->Add(content_sizer, 1, wxEXPAND);
+
+	if (button_id != wxID_NONE) {
+		auto *button = new wxButton(this, button_id);
+		button->SetFocus();
+		btn_sizer->Add(button);
+	}
+
+	rightsizer->Add(btn_sizer, 0, wxALIGN_RIGHT);
+
+	if (! bitmap.IsOk()) {
+		bitmap = create_scaled_bitmap("PrusaSlicer_192px.png", this, 192);
+	}
+
+	logo = new wxStaticBitmap(this, wxID_ANY, wxNullBitmap);
+
+	topsizer->Add(logo, 0, wxALL, BORDER);
+	topsizer->Add(rightsizer, 1, wxALL | wxEXPAND, BORDER);
+
+	SetSizerAndFit(topsizer);
+	*/
+}
+
+DesktopIntegrationDialog::~DesktopIntegrationDialog()
+{
+
+}
+
+} // namespace GUI
+} // namespace Slic3r
+#endif // __linux__
\ No newline at end of file
diff --git a/src/slic3r/GUI/DesktopIntegrationDialog.hpp b/src/slic3r/GUI/DesktopIntegrationDialog.hpp
new file mode 100644
index 000000000..74a0a68f9
--- /dev/null
+++ b/src/slic3r/GUI/DesktopIntegrationDialog.hpp
@@ -0,0 +1,39 @@
+#ifdef __linux__
+#ifndef slic3r_DesktopIntegrationDialog_hpp_
+#define slic3r_DesktopIntegrationDialog_hpp_
+
+#include <wx/dialog.h>
+
+namespace Slic3r {
+namespace GUI {
+class DesktopIntegrationDialog : public wxDialog
+{
+public:
+	DesktopIntegrationDialog(wxWindow *parent);
+	DesktopIntegrationDialog(DesktopIntegrationDialog &&) = delete;
+	DesktopIntegrationDialog(const DesktopIntegrationDialog &) = delete;
+	DesktopIntegrationDialog &operator=(DesktopIntegrationDialog &&) = delete;
+	DesktopIntegrationDialog &operator=(const DesktopIntegrationDialog &) = delete;
+	~DesktopIntegrationDialog();
+
+	// methods that actually do / undo desktop integration. Static to be accesible from anywhere.
+
+	// returns true if path to PrusaSlicer.desktop is stored in App Config and existence of desktop file. 
+	// Does not check if desktop file leads to this binary or existence of icons and viewer desktop file.
+	static bool is_integrated();
+	// true if appimage
+	static bool integration_possible();
+	// Creates Desktop files and icons for both PrusaSlicer and GcodeViewer.
+	// Stores paths into App Config.
+	// Rewrites if files already existed.
+	static void perform_desktop_integration();
+	// Deletes Desktop files and icons for both PrusaSlicer and GcodeViewer at paths stored in App Config.
+	static void undo_desktop_intgration();
+private:
+
+};
+} // namespace GUI
+} // namespace Slic3r
+
+#endif // slic3r_DesktopIntegrationDialog_hpp_
+#endif // __linux__
\ No newline at end of file
diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp
index 62961a867..cb7c2d24e 100644
--- a/src/slic3r/GUI/GUI_App.cpp
+++ b/src/slic3r/GUI/GUI_App.cpp
@@ -69,6 +69,7 @@
 #include "UnsavedChangesDialog.hpp"
 #include "SavePresetDialog.hpp"
 #include "PrintHostDialogs.hpp"
+#include "DesktopIntegrationDialog.hpp"
 
 #include "BitmapCache.hpp"
 
@@ -1634,6 +1635,10 @@ void GUI_App::add_config_menu(wxMenuBar *menu)
         local_menu->Append(config_id_base + ConfigMenuSnapshots, _L("&Configuration Snapshots") + dots, _L("Inspect / activate configuration snapshots"));
         local_menu->Append(config_id_base + ConfigMenuTakeSnapshot, _L("Take Configuration &Snapshot"), _L("Capture a configuration snapshot"));
         local_menu->Append(config_id_base + ConfigMenuUpdate, _L("Check for updates"), _L("Check for configuration updates"));
+#ifdef __linux__
+        if (DesktopIntegrationDialog::integration_possible())
+            local_menu->Append(config_id_base + ConfigMenuDesktopIntegration, _L("Desktop Integration"), _L("Desktop Integration"));    
+#endif        
         local_menu->AppendSeparator();
     }
     local_menu->Append(config_id_base + ConfigMenuPreferences, _L("&Preferences") + dots +
@@ -1674,6 +1679,11 @@ void GUI_App::add_config_menu(wxMenuBar *menu)
 		case ConfigMenuUpdate:
 			check_updates(true);
 			break;
+#ifdef __linux__
+        case ConfigMenuDesktopIntegration:
+            show_desktop_integration_dialog();
+            break;
+#endif
         case ConfigMenuTakeSnapshot:
             // Take a configuration snapshot.
             if (check_unsaved_changes()) {
@@ -2066,6 +2076,15 @@ bool GUI_App::run_wizard(ConfigWizard::RunReason reason, ConfigWizard::StartPage
     return res;
 }
 
+void GUI_App::show_desktop_integration_dialog()
+{
+#ifdef __linux__
+    //wxCHECK_MSG(mainframe != nullptr, false, "Internal error: Main frame not created / null");
+    DesktopIntegrationDialog dialog(mainframe);
+    dialog.ShowModal();
+#endif //__linux__
+}
+
 #if ENABLE_THUMBNAIL_GENERATOR_DEBUG
 void GUI_App::gcode_thumbnails_debug()
 {
diff --git a/src/slic3r/GUI/GUI_App.hpp b/src/slic3r/GUI/GUI_App.hpp
index f1ee0746a..bc030a1bf 100644
--- a/src/slic3r/GUI/GUI_App.hpp
+++ b/src/slic3r/GUI/GUI_App.hpp
@@ -76,6 +76,7 @@ enum ConfigMenuIDs {
     ConfigMenuSnapshots,
     ConfigMenuTakeSnapshot,
     ConfigMenuUpdate,
+    ConfigMenuDesktopIntegration,
     ConfigMenuPreferences,
     ConfigMenuModeSimple,
     ConfigMenuModeAdvanced,
@@ -268,6 +269,7 @@ public:
 
     void            open_web_page_localized(const std::string &http_address);
     bool            run_wizard(ConfigWizard::RunReason reason, ConfigWizard::StartPage start_page = ConfigWizard::SP_WELCOME);
+    void            show_desktop_integration_dialog();
 
 #if ENABLE_THUMBNAIL_GENERATOR_DEBUG
     // temporary and debug only -> extract thumbnails from selected gcode and save them as png files
diff --git a/src/slic3r/GUI/NotificationManager.hpp b/src/slic3r/GUI/NotificationManager.hpp
index 17657948e..1bcb93de0 100644
--- a/src/slic3r/GUI/NotificationManager.hpp
+++ b/src/slic3r/GUI/NotificationManager.hpp
@@ -82,7 +82,13 @@ enum class NotificationType
     // Notification emitted by Print::validate
     PrintValidateWarning,
     // Notification telling user to quit SLA supports manual editing
-    QuitSLAManualMode
+    QuitSLAManualMode,
+    // Desktop integration basic info
+    DesktopIntegrationSuccess,
+    DesktopIntegrationFail,
+    UndoDesktopIntegrationSuccess,
+    UndoDesktopIntegrationFail
+
 };
 
 class NotificationManager
@@ -514,6 +520,14 @@ private:
 				 "To export the G-code correctly, check the \"Color Change G-code\" in \"Printer Settings > Custom G-code\"") },
 		{NotificationType::EmptyAutoColorChange, NotificationLevel::RegularNotification, 10,  
 			_u8L("This model doesn't allow to automatically add the color changes") },
+		{NotificationType::DesktopIntegrationSuccess, NotificationLevel::RegularNotification, 10,  
+			_u8L("Desktop integration was successful.") },
+		{NotificationType::DesktopIntegrationFail, NotificationLevel::WarningNotification, 10,  
+			_u8L("Desktop integration failed.") },
+		{NotificationType::UndoDesktopIntegrationSuccess, NotificationLevel::RegularNotification, 10,  
+			_u8L("Undo desktop integration was successful.") },
+		{NotificationType::UndoDesktopIntegrationFail, NotificationLevel::WarningNotification, 10,  
+			_u8L("Undo desktop integration failed.") },
 		//{NotificationType::NewAppAvailable, NotificationLevel::ImportantNotification, 20,  _u8L("New vesion of PrusaSlicer is available.",  _u8L("Download page.") },
 		//{NotificationType::LoadingFailed, NotificationLevel::RegularNotification, 20,  _u8L("Loading of model has Failed") },
 		//{NotificationType::DeviceEjected, NotificationLevel::RegularNotification, 10,  _u8L("Removable device has been safely ejected")} // if we want changeble text (like here name of device), we need to do it as CustomNotification