summaryrefslogtreecommitdiff
path: root/gui-apps/wcm/files/wcm-0.11.0-window-scope.patch
blob: 1351162783e3e7dcd3fcc4467046581971b601f0 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
--- a/src/displays.cpp
+++ b/src/displays.cpp
@@ -103,20 +103,43 @@
     left.set_border_width(12); right.set_border_width(12);
     settings_tabs.append_page(left, "Desktop and window zones");
     settings_tabs.append_page(right, "Physical monitor");
+    settings_tabs.append_page(scope_tab, "Application scope");
+    scope_tab.set_border_width(12);
+    scope_help.set_text("Physical scope keeps maximize and fullscreen on one monitor. VIRTUAL spans the combined desktop. Dialogs follow their parent; moving a window updates its current monitor.");
+    scope_help.set_line_wrap(); scope_help.set_max_width_chars(65); scope_help.set_halign(Gtk::ALIGN_START);
+    scope_tab.pack_start(scope_help, false, false);
+    rules_help.set_text("Application exceptions — first matching rule wins. App IDs are case sensitive; * matches any text and ? matches one character. Use a running application to select its exact ID.");
+    rules_help.set_line_wrap(); rules_help.set_max_width_chars(65); rules_help.set_halign(Gtk::ALIGN_START);
+    scope_tab.pack_start(rules_help, false, false);
+    running_apps.get_accessible()->set_name("Running application ID");
+    rule_actions.pack_start(running_apps, true, true); rule_actions.pack_start(add_running, false, false);
+    scope_tab.pack_start(rule_actions, false, false); scope_tab.pack_start(rules_box, false, false);
+    scope_tab.pack_start(add_rule, false, false);
+    scope_shortcuts.set_row_spacing(8); scope_shortcuts.set_column_spacing(12);
+    field(scope_shortcuts, 0, "Use VIRTUAL shortcut", virtual_binding);
+    field(scope_shortcuts, 1, "Use physical monitor shortcut", physical_binding);
+    field(scope_shortcuts, 2, "Restore application policy shortcut", reset_binding);
+    field(scope_shortcuts, 3, "Move to next monitor shortcut", next_binding);
+    scope_tab.pack_start(scope_shortcuts, false, false);
+    add_rule.signal_clicked().connect([this] { append_rule("", "virtual"); });
+    add_running.signal_clicked().connect([this] { if (!running_apps.get_active_id().empty()) append_rule(running_apps.get_active_id(), "virtual"); });
     body.pack_start(settings_tabs, true, true);
     settings_scroll.add(body); settings_scroll.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC);
     settings_scroll.set_min_content_height(120);
     pack_start(settings_scroll, true, true);
     left.pack_start(combined, false, false);
     general_grid.set_row_spacing(10); general_grid.set_column_spacing(12);
-    field(general_grid, 0, "Default window destination", default_output);
+    field(general_grid, 0, "Default desktop output", default_output);
+    default_scope.append("physical", "Physical monitor"); default_scope.append("virtual", "VIRTUAL — combined desktop");
+    default_scope.get_accessible()->set_name("Default window scope");
+    field(general_grid, 4, "Default window scope", default_scope);
     gap.set_range(0, 1000); gap.set_increments(1, 5);
     gap.get_accessible()->set_name("Shared monitor-zone gap");
     field(general_grid, 1, "Monitor-zone outer gap (px)", gap);
     field(general_grid, 2, "Zone maximize shortcut", zone_binding);
     field(general_grid, 3, "Zone drag binding", drag_binding);
     left.pack_start(general_grid, false, false); left.pack_start(shift, false, false);
-    constraints.set_text("Combined monitors: horizontal arrangement, matching heights and refresh rates, scale 1, normal rotation, SDR/8-bit and fixed refresh. Ordinary maximize and fullscreen use the whole virtual desktop.");
+    constraints.set_text("Combined monitors: horizontal arrangement, matching heights and refresh rates, scale 1, normal rotation, SDR/8-bit and fixed refresh. Window scope controls whether maximize and fullscreen use one physical monitor or the combined desktop.");
     constraints.set_line_wrap(); constraints.set_max_width_chars(48); constraints.set_halign(Gtk::ALIGN_START);
     left.pack_start(constraints, false, false);
     left.pack_start(zones_button, false, false); left.pack_start(shell_button, false, false);
@@ -158,8 +181,8 @@
     monitors.signal_changed().connect(sigc::mem_fun(*this, &DisplaysPage::select_monitor));
     auto change = [this] { if (!loading) changed(); };
     for (auto *widget : {&combined, &include, &shift}) widget->signal_toggled().connect(change);
-    for (auto *widget : {&default_output, &role, &mode, &rotation}) widget->signal_changed().connect(change);
-    for (auto *widget : {&position, &zone_binding, &drag_binding}) widget->signal_changed().connect(change);
+    for (auto *widget : {&default_output, &role, &mode, &rotation, &default_scope}) widget->signal_changed().connect(change);
+    for (auto *widget : {&position, &zone_binding, &drag_binding, &virtual_binding, &physical_binding, &reset_binding, &next_binding}) widget->signal_changed().connect(change);
     scale.signal_value_changed().connect(change); gap.signal_value_changed().connect(change);
     local_gap.signal_value_changed().connect(change);
     auto move = [this] (int delta)
@@ -226,6 +249,14 @@
     wf::json_t opts = data["options"];
     for (auto& key : opts.get_member_names()) original[key] = opts[key].as_string();
     draft = original;
+    if (!draft.count("core/window_scope_rules"))
+    {
+        loading = false;
+        throw std::runtime_error("Application scope needs Wayfire r8 or later. Restart into the matching compositor after installing it.");
+    }
+    default_scope.set_active_id(get("core/window_scope"));
+    virtual_binding.set_text(get("grid/use_virtual")); physical_binding.set_text(get("grid/use_physical"));
+    reset_binding.set_text(get("grid/reset_scope")); next_binding.set_text(get("grid/next_monitor"));
     wf::json_t physical = data["physical"];
     for (size_t i = 0; i < physical.size(); i++)
     {
@@ -242,6 +273,32 @@
         }
         heads[h.name] = h; monitors.append(h.name, h.name);
     }
+    scope_rows.clear();
+    for (auto child : rules_box.get_children()) rules_box.remove(*child);
+    wf::json_t rules;
+    if (!wf::json_t::parse_string(get("core/window_scope_rules"), rules) && rules.is_array())
+        for (size_t i = 0; i < rules.size(); ++i)
+        {
+            wf::json_t rule = rules[i];
+            if (rule.is_object() && rule.has_member("app_id") && rule["app_id"].is_string() &&
+                rule.has_member("scope") && rule["scope"].is_string())
+                append_rule(rule["app_id"].as_string(), rule["scope"].as_string());
+        }
+    running_apps.remove_all();
+    try
+    {
+        auto views = request("window-rules/list-views"); std::set<std::string> apps;
+        for (size_t i = 0; i < views.size(); ++i)
+        {
+            wf::json_t view = views[i];
+            if (view["mapped"].as_bool() && view["role"].as_string() == "toplevel")
+            {
+                auto app = view["app-id"].as_string();
+                if (!app.empty() && apps.insert(app).second) running_apps.append(app, app);
+            }
+        }
+        running_apps.set_active(0);
+    } catch (...) {} // Rules remain editable when ipc-rules is disabled.
     std::istringstream input(get("core/stitch_outputs")); std::string name;
     while (input >> name) if (heads.count(name)) order.push_back(name);
     combined.set_active(get("core/stitch_enabled") == "true");
@@ -301,6 +358,9 @@
     if (loading) return;
     set("core/stitch_enabled", combined.get_active() ? "true" : "false");
     set("core/default_output", default_output.get_active_id());
+    set("core/window_scope", default_scope.get_active_id());
+    set("grid/use_virtual", virtual_binding.get_text()); set("grid/use_physical", physical_binding.get_text());
+    set("grid/reset_scope", reset_binding.get_text()); set("grid/next_monitor", next_binding.get_text());
     set("grid/zone_gap", std::to_string(gap.get_value_as_int()));
     set("grid/zone_shift", shift.get_active() ? "true" : "false");
     set("grid/zone_maximize", zone_binding.get_text()); set("move/activate_zone", drag_binding.get_text());
@@ -443,3 +503,56 @@
         identify_timer = Glib::signal_timeout().connect([this] { identifiers.clear(); return false; }, 3000);
     } catch (const std::exception& e) { error(e.what()); }
 }
+
+
+void DisplaysPage::append_rule(const std::string& app, const std::string& scope)
+{
+    auto row = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, 6));
+    auto entry = Gtk::manage(new Gtk::Entry());
+    auto choice = Gtk::manage(new Gtk::ComboBoxText());
+    auto up = Gtk::manage(new Gtk::Button("↑"));
+    auto down = Gtk::manage(new Gtk::Button("↓"));
+    auto remove = Gtk::manage(new Gtk::Button("Remove"));
+    entry->set_text(app); entry->set_placeholder_text("Application ID or pattern");
+    entry->get_accessible()->set_name("Application ID pattern"); entry->set_hexpand(true);
+    choice->append("virtual", "VIRTUAL"); choice->append("physical", "Current monitor");
+    for (auto& [name, head] : heads)
+        if (get(head.prefix + "masked") != "true") choice->append(name, name);
+    if (!choice->set_active_id(scope)) { choice->append(scope, scope + " (unavailable)"); choice->set_active_id(scope); }
+    choice->get_accessible()->set_name("Application window scope");
+    up->set_tooltip_text("Move rule earlier"); down->set_tooltip_text("Move rule later");
+    row->pack_start(*entry, true, true); row->pack_start(*choice, false, false);
+    row->pack_start(*up, false, false); row->pack_start(*down, false, false); row->pack_start(*remove, false, false);
+    rules_box.pack_start(*row, false, false); scope_rows.push_back({row, entry, choice});
+    auto move = [this, row] (int delta)
+    {
+        auto it = std::find_if(scope_rows.begin(), scope_rows.end(), [row] (auto& r) { return r.row == row; });
+        int i = it - scope_rows.begin(), target = i + delta;
+        if (it == scope_rows.end() || target < 0 || target >= (int)scope_rows.size()) return;
+        std::swap(scope_rows[i], scope_rows[target]);
+        for (size_t n = 0; n < scope_rows.size(); ++n) rules_box.reorder_child(*scope_rows[n].row, n);
+        store_rules();
+    };
+    up->signal_clicked().connect([move] { move(-1); }); down->signal_clicked().connect([move] { move(1); });
+    remove->signal_clicked().connect([this, row]
+    {
+        scope_rows.erase(std::remove_if(scope_rows.begin(), scope_rows.end(), [row] (auto& r) { return r.row == row; }), scope_rows.end());
+        rules_box.remove(*row); store_rules();
+    });
+    entry->signal_changed().connect([this] { store_rules(); }); choice->signal_changed().connect([this] { store_rules(); });
+    row->show_all();
+    if (!loading) { store_rules(); entry->grab_focus(); }
+}
+
+void DisplaysPage::store_rules()
+{
+    if (loading) return;
+    wf::json_t rules = wf::json_t::array();
+    for (auto& row : scope_rows)
+    {
+        wf::json_t rule;
+        rule["app_id"] = row.app->get_text().raw(); rule["scope"] = row.scope->get_active_id().raw();
+        rules.append(rule);
+    }
+    set("core/window_scope_rules", rules.serialize()); changed();
+}
--- a/src/displays.hpp
+++ b/src/displays.hpp
@@ -25,7 +25,16 @@
     Gtk::Button shell_button{"Panel Settings"}, zones_button{"More Window-Zone Settings"};
     Gtk::CheckButton combined{"Combine selected monitors into VIRTUAL-1"}, include{"Include in combined desktop"};
     Gtk::CheckButton shift{"Shift-maximize uses a physical monitor"};
-    Gtk::ComboBoxText default_output, monitors, role, mode, rotation;
+    Gtk::ComboBoxText default_output, monitors, role, mode, rotation, default_scope, running_apps;
+    Gtk::Box scope_tab{Gtk::ORIENTATION_VERTICAL, 10}, rules_box{Gtk::ORIENTATION_VERTICAL, 6}, rule_actions{Gtk::ORIENTATION_HORIZONTAL, 8};
+    Gtk::Label scope_help, rules_help;
+    Gtk::Button add_rule{"Add Rule"}, add_running{"Add Selected Application"};
+    Gtk::Grid scope_shortcuts;
+    Gtk::Entry virtual_binding, physical_binding, reset_binding, next_binding;
+    struct ScopeRule { Gtk::Box *row; Gtk::Entry *app; Gtk::ComboBoxText *scope; };
+    std::vector<ScopeRule> scope_rows;
+    void append_rule(const std::string& app, const std::string& scope);
+    void store_rules();
     Gtk::Entry position, zone_binding, drag_binding;
     Gtk::SpinButton scale, gap, local_gap;
     Gtk::Grid general_grid, monitor_grid;