diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1bf7786..f2364af 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,20 @@
-# 0.1.0
+# Change Log
+
+## 0.2.0
+
+- :bug: Fixed: Window loses focus after being moved to new desktop.
+- :sparkles: Feature: Trigger by window maximize
+- :sparkles: Feature: Customizable new desktop position
+- :sparkles: Feature: Keep non-empty desktop
+- :sparkles: Feature: Skip blacklisted windows
+- :sparkles: Feature: Configuration UI
+- :recycle: Improved: Complete rewrite.
+
+## 0.1.0
+
- :bug: Fixed: No more extra VD after closing fullscreened window directly.
- :recycle: Improved: Code refactorings.
-# 0.0.1
+## 0.0.1
+
- Initial release. Mostly functional.
diff --git a/README.md b/README.md
index 9d2cc36..a7ba46d 100644
--- a/README.md
+++ b/README.md
@@ -14,11 +14,11 @@ The config panel
## Feature
-* Move window to a newly created virtual desktop when fullscreen.
+* Move window to a newly created virtual desktop when fullscreen/maximize.
* Move window back to original desktop when restored to normal size or closed.
-* Configurable trigger: fullscreen or maximize or _both_.
-* Configurable new desktop position: right most or right to current.
-* Window blacklist. Skip some windows that should not trigger the script.
+* Configurable trigger: `fullscreen` or `maximize` or `both`.
+* Configurable new desktop position: `right most` or `next to current desktop` or `next to app`.
+* Window blacklist: windows that match the class name will not trigger the script.
__Note__:
The default behavior is triggered by window **FULLSCREEN**, not the normal maximize. Window fullscreen can be enabled by right clicking on the window decoration -> `More Actions` -> `Fullscreen`.
diff --git a/contents/code/main.js b/contents/code/main.js
index e0e4af9..bfb6b98 100644
--- a/contents/code/main.js
+++ b/contents/code/main.js
@@ -1,89 +1,345 @@
-var state = {
- savedDesktops: {},
- enabled: true
-};
-
function log(msg) {
print("KWinMax2NewVirtualDesktop: " + msg);
}
-function moveToNewDesktop(client) {
- state.savedDesktops[client.windowId] = client.desktop;
+/*
+ * Enum that should match the config in main.xml
+ */
+const TriggerValues = {
+ FullscreenOnly: 0,
+ MaximizeOnly: 1,
+ FullscreenAndMaximize: 2
+};
+/*
+ * Enum that should match the config in main.xml
+ */
+const NewDesktopPositionValue = {
+ RightMost: 0,
+ NextToCurrent: 1,
+ NextToApp: 2,
+};
- var next = workspace.desktops + 1;
- workspace.desktops = next;
- client.desktop = next;
- workspace.currentDesktop = next;
- workspace.activateClient = client;
+function Config() {
}
+Config.prototype.trigger = function() {
+ var v = readConfig('trigger', 'FullscreenOnly');
+ return TriggerValues[v];
+};
+Config.prototype.newDesktopPosition = function() {
+ var v = readConfig("newDesktopPosition", 'RightMost');
+ return NewDesktopPositionValue[v];
+};
-function moveBack(client) {
- var saved = state.savedDesktops[client.windowId];
- if (saved === undefined) {
- log("Ignoring window not previously seen: " + client.caption);
- } else {
- log("Resotre client desktop to " + saved);
- client.desktop = saved;
- workspace.currentDesktop = saved;
- workspace.activateClient = client;
+Config.prototype.keepNonEmptyDesktop = function() {
+ var v = readConfig("keepNonEmptyDesktop", false);
+ // convert to primitive value
+ return Boolean.prototype.valueOf(v);
+};
- workspace.desktops -= 1;
+Config.prototype.blockWMClass = function() {
+ var classes = readConfig("blockWMClass", "").toString();
+ classes = classes.split(",");
+ return classes;
+};
+
+function State() {
+ this.savedDesktops = {};
+ this.enabled = true;
+ this.config = new Config();
+}
+
+State.prototype.isTriggeredByFull = function() {
+ return this.config.trigger() === TriggerValues.FullscreenOnly
+ || this.config.trigger() === TriggerValues.FullscreenAndMaximize;
+}
+State.prototype.isTriggeredByMax = function() {
+ return this.config.trigger() === TriggerValues.MaximizeOnly
+ || this.config.trigger() === TriggerValues.FullscreenAndMaximize;
+}
+
+State.prototype.isKnownClient = function(client) {
+ return this.savedDesktops.hasOwnProperty(client.windowId);
+}
+
+State.prototype.isSkippedClient = function (client) {
+ var idx = this.config.blockWMClass().indexOf(client.resourceClass.toString());
+ return idx != -1;
+}
+
+State.prototype.debugDump = function() {
+ log('');
+ log('state: enabled ' + this.enabled);
+ log('state: triggerFull ' + this.isTriggeredByFull());
+ log('state: triggerMax ' + this.isTriggeredByMax());
+ log('state: newPosition ' + this.config.newDesktopPosition());
+ log('state: blockWMClass ' + this.config.blockWMClass().toString());
+ log('state: savedDesktops size ' + Object.keys(this.savedDesktops).length);
+ for (var client in this.savedDesktops) {
+ log('state: savedDesktops: ' + client + ' => ' + this.savedDesktops[client]);
+ }
+ log('');
+}
+
+function Main() {
+ this.state = new State();
+
+ // signal handler's this will be the global object
+ var self = this;
+
+ this.handlers = {
+ fullscreen: function(client, full) {
+ log('handle fullscreen');
+ self.state.debugDump();
+ if (!self.state.isTriggeredByFull() || self.state.isSkippedClient(client)) {
+ log('handle fullscreen return');
+ return;
+ }
+ if (full) {
+ self.moveToNewDesktop(client);
+ } else {
+ log("moving back: " + client.caption);
+ self.moveBack(client);
+ }
+ self.state.debugDump();
+ log('handle fullscreen done');
+ },
+
+ maximize: function(client, h, v) {
+ log('handle maximize');
+ self.state.debugDump();
+ if (!self.state.isTriggeredByMax() || self.state.isSkippedClient(client)) {
+ log('handle maximize return');
+ return;
+ }
+ if (h && v) {
+ self.moveToNewDesktop(client);
+ } else {
+ self.moveBack(client);
+ }
+ self.state.debugDump();
+ log('handle maximize done');
+ },
+
+ removed: function(client) {
+ log('handle remove');
+ self.state.debugDump();
+ if (!self.state.isKnownClient(client) || self.state.isSkippedClient(client)) {
+ log('handle remove return');
+ return;
+ }
+ self.moveBack(client);
+ self.state.debugDump();
+ log('handle remove done');
+ },
+
+ createUserActionsMenu: function(client) {
+ log("Creating user menu");
+ self.state.debugDump();
+ return {
+ text: "Maximize to New Desktop",
+ items: [
+ {
+ text: "Enabled",
+ checkable: true,
+ checked: self.state.enabled,
+ triggered: function() {
+ self.state.enabled = !self.state.enabled;
+ if (self.state.enabled) {
+ self.install();
+ } else {
+ self.uninstall();
+ }
+ }
+ },
+ {
+ text: 'FullTriggered',
+ checkable: true,
+ checked: self.state.isTriggeredByFull(),
+ triggered: function () {
+ return;
+ }
+ },
+ {
+ text: 'MaxTriggered',
+ checkable: true,
+ checked: self.state.isTriggeredByMax(),
+ triggered: function () {
+ return;
+ }
+ },
+ {
+ text: 'KeepNonEmpty',
+ checkable: true,
+ checked: self.state.config.keepNonEmptyDesktop(),
+ triggered: function () {
+ return;
+ }
+ },
+ ]
+ }
+ }
+ }
+}
+
+Main.prototype.getNextDesktop = function (client) {
+ switch (this.state.config.newDesktopPosition()) {
+ case NewDesktopPositionValue.RightMost:
+ log('RightMost, workspace.desktops is ' + workspace.desktops);
+ return workspace.desktops + 1;
+ case NewDesktopPositionValue.NextToCurrent:
+ log('NextToCurrent, workspace.currentDesktop is ' + workspace.currentDesktop);
+ return workspace.currentDesktop + 1;
+ case NewDesktopPositionValue.NextToApp:
+ log('NextToApp, client.desktop is ' + client.desktop);
+ return client.desktop + 1;
+ default:
+ log('default, workspace.desktops is ' + workspace.desktops);
+ return workspace.desktops + 1;
}
}
-function fullHandler(client, full, user) {
- if (full) {
- moveToNewDesktop(client);
+// If the desktop at pos can be removed
+Main.prototype.shouldRemoveDesktop = function(pos) {
+ if (pos <= 0) {
+ return false;
+ }
+
+ if (!this.state.config.keepNonEmptyDesktop()) {
+ return true;
+ }
+
+ // only remove if the desktop is empty
+ var count = 0;
+ const clients = workspace.clientList();
+ for (var i = 0; i < clients.length; i++) {
+ if (clients[i].desktop == pos) {
+ count++;
+ }
+ }
+ return count == 0;
+};
+
+// shift clients' desktop on desktop [from, to], to direction
+Main.prototype.shiftClients = function (direction, from, to) {
+ to = typeof to !== 'undefined' ? to : workspace.desktops;
+
+ // note that client.desktop is 1-based.
+ const clients = workspace.clientList();
+ for (var i = 0; i < clients.length; i++) {
+ if (clients[i].desktop >= from && clients[i].desktop <= to) {
+ clients[i].desktop += direction;
+ }
+ }
+ // also updated saved desktop info
+ for (var clientId in this.state.savedDesktops) {
+ if (this.state.savedDesktops[clientId] >= from
+ && this.state.savedDesktops[clientId] <= to) {
+ this.saved.savedDesktops[clientId] += direction;
+ }
+ }
+}
+
+// Insert a new desktop at pos and switch to it, which is 1-based.
+// if pos > workspace.desktops, new desktops will be created
+// if pos <= workspace.desktops, new desktops will be created at end
+// and clients will be shifted
+Main.prototype.insertDesktop = function (pos) {
+ log("Inserting a desktop at " + pos);
+ // save old current
+ var oldCurrent = workspace.currentDesktop;
+ if (oldCurrent >= pos) {
+ oldCurrent += 1;
+ }
+
+ if (pos > workspace.desktops) {
+ workspace.desktops = pos;
} else {
- moveBack(client);
+ // add one desktop and shift clients
+ workspace.desktops += 1;
+ this.shiftClients(+1, pos);
}
+
+ // switch to new desktop
+ workspace.currentDesktop = pos;
+
+ return pos;
}
-function rmHandler(client) {
- moveBack(client);
+// remove desktop at pos, shifting clients as needed
+Main.prototype.popDesktop = function (pos) {
+ log('popDesktop: ' + pos);
+ if (pos > workspace.desktops) {
+ log('popDesktop: pos > workspace.desktops: ' + workspace.desktops);
+ return;
+ }
+
+ if (!this.shouldRemoveDesktop(pos)) {
+ log('popDesktop: should not remove desktop');
+ return;
+ }
+
+ if (pos == workspace.desktops) {
+ log('popDesktop: removing');
+ workspace.desktops -= 1;
+ } else {
+ log('popDesktop: shifting');
+ this.shiftClients(-1, pos);
+ log('popDesktop: removing');
+ workspace.desktops -= 1;
+ }
+}
+
+Main.prototype.moveToNewDesktop = function(client) {
+ this.state.savedDesktops[client.windowId] = client.desktop;
+
+ var next = this.getNextDesktop(client);
+ this.insertDesktop(next);
+ client.desktop = next;
+
+ // make sure the client is activated
+ workspace.activeClient = client;
}
-function install() {
- workspace.clientFullScreenSet.connect(fullHandler);
- workspace.clientRemoved.connect(rmHandler);
+Main.prototype.moveBack = function(client) {
+ if (!this.state.isKnownClient(client)) {
+ log("Ignoring window not previously seen: " + client.caption);
+ return;
+ }
+
+ log("inside moving back: " + client.caption);
+ var saved = this.state.savedDesktops[client.windowId];
+ var toRemove = client.desktop;
+
+ log("Resotre client desktop to " + saved);
+ client.desktop = saved;
+ workspace.currentDesktop = saved;
+ workspace.activeClient = client;
+ delete this.state.savedDesktops[client.windowId];
+
+ this.popDesktop(toRemove);
+}
+
+
+Main.prototype.install = function() {
+ workspace.clientFullScreenSet.connect(this.handlers.fullscreen);
+ workspace.clientMaximizeSet.connect(this.handlers.maximize);
+ workspace.clientRemoved.connect(this.handlers.removed);
log("Handler installed");
}
-function uninstall() {
- workspace.clientFullScreenSet.disconnect(fullHandler);
- workspace.clientRemoved.disconnect(rmHandler);
+Main.prototype.uninstall = function() {
+ workspace.clientFullScreenSet.disconnect(this.handlers.fullscreen);
+ workspace.clientMaximizeSet.disconnect(this.handlers.maximize);
+ workspace.clientRemoved.disconnect(this.handlers.removed);
log("Handler cleared");
}
-registerUserActionsMenu(function(client){
- return {
- text: "Maximize to New Desktop",
- items: [
- {
- text: "Enabled",
- checkable: true,
- checked: state.enabled,
- triggered: function() {
- state.enabled = !state.enabled;
- if (state.enabled) {
- install();
- } else {
- uninstall();
- }
- }
- },
- /*
- {
- text: "Disable for this window",
- checkable: true,
- checked: false,
- triggered: function(act) {
- log('Not implemented yet!');
- }
- }
- */
- ]
- };
-});
+Main.prototype.init = function() {
+ // this.state.debugDump();
+ // registerUserActionsMenu(this.handlers.createUserActionsMenu);
+ // this.state.debugDump();
+ this.install();
+};
-install();
+main = new Main();
+main.init();
\ No newline at end of file
diff --git a/contents/config/main.xml b/contents/config/main.xml
index eb3721c..67ab6ba 100644
--- a/contents/config/main.xml
+++ b/contents/config/main.xml
@@ -19,7 +19,8 @@
-
+
+
diff --git a/contents/ui/config.ui b/contents/ui/config.ui
index 4bd00a2..daafbb8 100644
--- a/contents/ui/config.ui
+++ b/contents/ui/config.ui
@@ -95,7 +95,12 @@
-
- Right To Current
+ Next To Current
+
+
+ -
+
+ Next To App