diff --git a/usr/lib/webapp-manager/common.py b/usr/lib/webapp-manager/common.py
index 58a6b80..db7cab3 100644
--- a/usr/lib/webapp-manager/common.py
+++ b/usr/lib/webapp-manager/common.py
@@ -92,14 +92,18 @@ def __init__(self, path, codename):
self.navbar = False
self.privatewindow = False
- is_webapp = False
+ has_startup_wmclass = False
+ has_webapp_metadata = False
+ has_legacy_webapp_wmclass = False
with open(path) as desktop_file:
for line in desktop_file:
line = line.strip()
# Identify if the app is a webapp
- if "StartupWMClass=WebApp" in line or "StartupWMClass=Chromium" in line or "StartupWMClass=ICE-SSB" in line:
- is_webapp = True
+ if line.startswith("StartupWMClass="):
+ has_startup_wmclass = True
+ if "StartupWMClass=WebApp" in line or "StartupWMClass=Chromium" in line or "StartupWMClass=ICE-SSB" in line:
+ has_legacy_webapp_wmclass = True
continue
if "Name=" in line:
@@ -125,29 +129,36 @@ def __init__(self, path, codename):
continue
if "X-WebApp-Browser=" in line:
+ has_webapp_metadata = True
self.web_browser = line.replace("X-WebApp-Browser=", "")
continue
if "X-WebApp-URL=" in line:
+ has_webapp_metadata = True
self.url = line.replace("X-WebApp-URL=", "")
continue
if "X-WebApp-CustomParameters" in line:
+ has_webapp_metadata = True
self.custom_parameters = line.replace("X-WebApp-CustomParameters=", "")
continue
if "X-WebApp-Isolated" in line:
+ has_webapp_metadata = True
self.isolate_profile = line.replace("X-WebApp-Isolated=", "").lower() == "true"
continue
if "X-WebApp-Navbar" in line:
+ has_webapp_metadata = True
self.navbar = line.replace("X-WebApp-Navbar=", "").lower() == "true"
continue
if "X-WebApp-PrivateWindow" in line:
+ has_webapp_metadata = True
self.privatewindow = line.replace("X-WebApp-PrivateWindow=", "").lower() == "true"
continue
+ is_webapp = has_legacy_webapp_wmclass or (has_startup_wmclass and has_webapp_metadata)
if is_webapp and self.name is not None and self.icon is not None:
self.is_valid = True
@@ -469,6 +480,19 @@ def edit_webapp(self, path, name, desc, browser, url, icon, category, custom_par
with open(path, 'w') as configfile:
config.write(configfile, space_around_delimiters=False)
+ def update_startup_wmclass(self, path, startup_wmclass):
+ config = configparser.RawConfigParser()
+ config.optionxform = str
+ config.read(path)
+
+ if not config.has_section("Desktop Entry"):
+ return
+
+ config.set("Desktop Entry", "StartupWMClass", startup_wmclass)
+
+ with open(path, 'w') as configfile:
+ config.write(configfile, space_around_delimiters=False)
+
def bool_to_string(boolean):
if boolean:
return "true"
diff --git a/usr/lib/webapp-manager/webapp-manager.py b/usr/lib/webapp-manager/webapp-manager.py
index d6aeb3e..d46dd68 100755
--- a/usr/lib/webapp-manager/webapp-manager.py
+++ b/usr/lib/webapp-manager/webapp-manager.py
@@ -4,6 +4,7 @@
import gettext
import locale
import os
+import re
import shutil
import subprocess
import warnings
@@ -64,6 +65,7 @@ def __init__(self, application):
self.settings = Gio.Settings(schema_id="org.x.webapp-manager")
self.manager = WebAppManager()
self.selected_webapp = None
+ self.detecting_dialog = None
self.icon_theme = Gtk.IconTheme.get_default()
# Set the Glade file
@@ -87,6 +89,7 @@ def __init__(self, application):
self.remove_button = self.builder.get_object("remove_button")
self.edit_button = self.builder.get_object("edit_button")
self.run_button = self.builder.get_object("run_button")
+ self.run_detect_button = self.builder.get_object("run_detect_button")
self.ok_button = self.builder.get_object("ok_button")
self.name_entry = self.builder.get_object("name_entry")
self.desc_entry = self.builder.get_object("desc_entry")
@@ -114,6 +117,7 @@ def __init__(self, application):
self.remove_button.connect("clicked", self.on_remove_button)
self.edit_button.connect("clicked", self.on_edit_button)
self.run_button.connect("clicked", self.on_run_button)
+ self.run_detect_button.connect("clicked", self.on_run_detect_button)
self.ok_button.connect("clicked", self.on_ok_button)
self.favicon_button.connect("clicked", self.on_favicon_button)
self.name_entry.connect("changed", self.on_name_entry)
@@ -267,6 +271,7 @@ def on_webapp_selected(self, selection):
self.remove_button.set_sensitive(True)
self.edit_button.set_sensitive(True)
self.run_button.set_sensitive(True)
+ self.run_detect_button.set_sensitive(True)
def on_webapp_activated(self, treeview, path, column):
self.run_webapp(self.selected_webapp)
@@ -306,6 +311,111 @@ def run_webapp(self, webapp):
def on_run_button(self, widget):
self.run_webapp(self.selected_webapp)
+ def on_run_detect_button(self, widget):
+ if self.selected_webapp is None:
+ return
+
+ self.run_webapp(self.selected_webapp)
+ self.detect_startup_wmclass(self.selected_webapp)
+
+ @_async
+ def detect_startup_wmclass(self, webapp):
+ if webapp is None:
+ return
+
+ self.show_detecting_popup()
+
+ if shutil.which("xprop") is None:
+ self.close_detecting_popup()
+ self.show_info_message(
+ _("xprop Not Found"),
+ _("The 'xprop' command is not installed."),
+ _("Install xprop (x11-utils) to use WM_CLASS detection."))
+ return
+
+ try:
+ result = subprocess.run(["xprop", "WM_CLASS"], capture_output=True, text=True)
+ except Exception as e:
+ self.close_detecting_popup()
+ self.show_info_message(
+ _("WM_CLASS Detection Failed"),
+ str(e),
+ None)
+ return
+
+ if result.returncode != 0:
+ message = result.stderr.strip() if result.stderr else _("xprop returned an error.")
+ self.close_detecting_popup()
+ self.show_info_message(
+ _("WM_CLASS Detection Failed"),
+ message,
+ None)
+ return
+
+ wmclass_match = re.search(r'WM_CLASS\(STRING\)\s*=\s*"([^"]+)"', result.stdout)
+ if wmclass_match is None:
+ self.close_detecting_popup()
+ self.show_info_message(
+ _("WM_CLASS Detection Failed"),
+ _("Could not parse WM_CLASS output."),
+ result.stdout.strip())
+ return
+
+ startup_wmclass = wmclass_match.group(1)
+ self.manager.update_startup_wmclass(webapp.path, startup_wmclass)
+ self.on_startup_wmclass_updated(startup_wmclass)
+
+ @idle
+ def show_detecting_popup(self):
+ if self.detecting_dialog is not None:
+ return
+
+ dialog = Gtk.MessageDialog(
+ transient_for=self.window,
+ flags=0,
+ message_type=Gtk.MessageType.INFO,
+ buttons=Gtk.ButtonsType.NONE,
+ text=_("Detecting StartupWMClass"))
+ dialog.format_secondary_text(
+ _("Click the launched web app window to capture WM_CLASS.\n\nIf you click another window, the wrong class may be saved."))
+ dialog.set_modal(False)
+ dialog.show_all()
+ self.detecting_dialog = dialog
+
+ @idle
+ def close_detecting_popup(self):
+ if self.detecting_dialog is not None:
+ self.detecting_dialog.destroy()
+ self.detecting_dialog = None
+
+ @idle
+ def show_info_message(self, title, text, secondary_text=None):
+ dialog = Gtk.MessageDialog(
+ transient_for=self.window,
+ flags=0,
+ message_type=Gtk.MessageType.INFO,
+ buttons=Gtk.ButtonsType.OK,
+ text=title)
+ dialog.format_secondary_text(text)
+ if secondary_text:
+ dialog.format_secondary_text("%s\n\n%s" % (text, secondary_text))
+ dialog.run()
+ dialog.destroy()
+
+ @idle
+ def on_startup_wmclass_updated(self, startup_wmclass):
+ self.load_webapps()
+ dialog = Gtk.MessageDialog(
+ transient_for=self.window,
+ flags=0,
+ message_type=Gtk.MessageType.INFO,
+ buttons=Gtk.ButtonsType.OK,
+ text=_("StartupWMClass Updated"))
+ dialog.format_secondary_text(_("StartupWMClass was set to '%s'.") % startup_wmclass)
+ dialog.run()
+ dialog.destroy()
+ self.close_detecting_popup()
+
def on_ok_button(self, widget):
category = self.category_combo.get_model()[self.category_combo.get_active()][CATEGORY_ID]
browser = self.browser_combo.get_model()[self.browser_combo.get_active()][BROWSER_OBJ]
@@ -514,6 +624,7 @@ def load_webapps(self):
self.remove_button.set_sensitive(False)
self.edit_button.set_sensitive(False)
self.run_button.set_sensitive(False)
+ self.run_detect_button.set_sensitive(False)
webapps = self.manager.get_webapps()
for webapp in webapps:
diff --git a/usr/share/webapp-manager/webapp-manager.ui b/usr/share/webapp-manager/webapp-manager.ui
index f08bd9f..ae75f03 100644
--- a/usr/share/webapp-manager/webapp-manager.ui
+++ b/usr/share/webapp-manager/webapp-manager.ui
@@ -150,6 +150,27 @@
3
+
+
+
+ False
+ True
+ 4
+
+