├── .babelrc ├── .bowerrc ├── .editorconfig ├── .gitattributes ├── .gitignore ├── .yo-rc.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── app ├── _locales │ ├── de │ │ └── messages.json │ └── en │ │ └── messages.json ├── images │ ├── icon-128.png │ ├── icon-16.png │ ├── icon-19.png │ ├── icon-38.png │ └── options-loader.gif ├── manifest.json ├── options.html ├── popup.html ├── scripts.babel │ ├── background.js │ ├── chromereload.js │ ├── contentscript.js │ ├── options.js │ └── popup.js └── styles │ ├── bootstrap.css.map │ └── main.css ├── bower.json ├── gulpfile.babel.js ├── package-lock.json ├── package.json └── test ├── index.html └── spec └── test.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"] 3 | } 4 | -------------------------------------------------------------------------------- /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "app/bower_components", 3 | "strict-ssl": false, 4 | "registry": { 5 | "search": [ 6 | "https://registry.bower.io" 7 | ] 8 | }, 9 | "timeout": 300000 10 | } 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | 10 | # Change these settings to your own preference 11 | indent_style = space 12 | indent_size = 2 13 | 14 | [*.json] 15 | indent_size = 2 16 | 17 | # We recommend you to keep these unchanged 18 | end_of_line = lf 19 | charset = utf-8 20 | trim_trailing_whitespace = true 21 | insert_final_newline = true 22 | 23 | [*.md] 24 | trim_trailing_whitespace = false 25 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | temp 3 | .tmp 4 | dist 5 | .sass-cache 6 | app/bower_components 7 | test/bower_components 8 | package 9 | app/scripts 10 | 11 | .idea/ 12 | -------------------------------------------------------------------------------- /.yo-rc.json: -------------------------------------------------------------------------------- 1 | { 2 | "generator-mocha": { 3 | "ui": "bdd", 4 | "rjs": false 5 | } 6 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | # 1.0.16 (2019-12-March) 3 | 4 | * New: add full screen maximized mode support. 5 | 6 | 7 | # 1.0.15 (2018-20-June) 8 | 9 | * Proper fix for maximized mode support. 10 | * Improve monitor detection overlay, its now in max mode. For Win10, due to DPI misaligment issue, its even better, as no misaligment exists in max mode. 11 | 12 | 13 | # 1.0.14 (2018-20-June) 14 | 15 | * Revert maximized mode. 16 | 17 | 18 | # 1.0.12 (2018-20-June) 19 | 20 | * New: custom positions can be set. Click the 'more options' to create additional custom positions, and select them if needed. 21 | * Fixed maximized mode. 22 | 23 | 24 | # 1.0.11 (2018-2-April) 25 | 26 | * Usability improvements 27 | * Easy help button 28 | * Quick info section 29 | * Help / Getting started section 30 | * Corporate branding 31 | * More error handling in internal code. 32 | * Use newer features of JavaScript ES5. 33 | * Upgrade internal libraries. That include webkit bugfixes. 34 | * jquery: ~3.1.0 -> ~3.3.1, 35 | * lodash: 4.16.4 -> 4.17.5, 36 | * angular: 1.5.8 -> ~1.6.9, 37 | * angular-resource: 1.5.8 -> ~1.6.9, 38 | * font-awesome: ~4.6.3 -> ~4.7.0, 39 | * angular-bootstrap: 2.2.0 -> ~2.5.0, 40 | * file-saver: 1.3.3 -> ~1.3.8, 41 | * angular-bootstrap-checkbox: 0.4.0 -> ~0.5.1 42 | * angular-intro.js: ~3.3.0 43 | 44 | 45 | 46 | # 1.0.10 (2016-13-December) 47 | 48 | * Add ability to do batch changes. For example, if you want to change the monitor or position to all rules. 49 | * Usability: Ability to close the monitor detection by pressing "Esc" key 50 | 51 | 52 | # 1.0.8 (2016-12-December) 53 | 54 | * Fixes Name of "Monitor" does not always reflect the number. 55 | * Usability: Reduce duration of monitor detection to 3 seconds. 56 | * Fixes enforce order of rules when import template enhancement. 57 | * Usability: Save-Button does not close window. 58 | 59 | 60 | # 1.0.6 (2016-01-December) 61 | 62 | * Usability: reorganize the toolbar, and include the undo button. 63 | * Fixed after template import and restarting the page, unexpected(default) rule templates are shown. 64 | 65 | 66 | # 1.0.4 (2016-01-December) 67 | 68 | * Usability: make more visible when changes are not saved. 69 | * Fixed when importing a template, unsaved changes hint was not being displayed. 70 | * Usability: Increased the delay when showing the monitor detection. 71 | * Usability: add ability to quickly change the main settings from an existent rule. 72 | * Fixed when in advanced more the options page would become wider, so that table fits. 73 | * Improve german translations. (thanks Pascal) 74 | 75 | # 1.0.2 (2016-30-November) 76 | 77 | * Smoother transition when the options page is loading 78 | 79 | 80 | 81 | # 1.0.1 (2016-17-November) 82 | 83 | Initial implementation that includes: 84 | * Flexible positioning options via Rules concept 85 | * Multi-monitor support 86 | * Validation of rules configuration against existent Monitor. 87 | * Monitor detection 88 | * Manual positioning detection that saves into rules. 89 | * Default monitor support. 90 | * Templates have unique code for any option in order to support template merging during import. 91 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 ControlExpert 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Chrome MultiWindow Positioner 2 | 3 | Tool extension that enables effective window positioning/placement in multi-monitor setups. 4 | 5 | ## Features 6 | * Flexible positioning options via Rules concept 7 | * Multi-monitor support 8 | * Validation of rules configuration against existent Monitor. 9 | * Monitor detection 10 | * Configuration templates support. It enables user profiles and larger organization distributed environments. 11 | * Manual positioning detection that saves into rules. 12 | * Default monitor support. 13 | 14 | ## Installation 15 | 16 | 1. Under the following address (https://goo.gl/bxuw3E) you will find the *MultiWindow Positioner* 17 | 2. Click the **ADD TO CHROME** button and then the **ADD EXTENSION** button. 18 | 3. The extension installation will take some seconds and you need to configure it. The configuration is available under either: 19 | * the following address: *chrome-extension://hmgehpjpfhobbnhhelhlggjfcaollidl/options.html* 20 | * or goint to the chrome://extensions/ und opening the **Options** link. 21 | 4. You may, at first, import a rules template 22 | * Click the **IMPORT TEMPLATE** icon 23 | * Give the following URL: https://cdn.rawgit.com/ControlExpert/chrome-multiwindow-positioner/gh-pages/templates/default-template-options.json 24 | * Click **ADD** to complete the dialog. 25 | * Finally click **SAVE** to save all the changes permanently. 26 | 5. If you need to to use other monitors for specific rules/websites you may edit/add a rule respectively. 27 | 6. In the edit or create rule dialog you may: 28 | * *Template*: List all available rule-templates. 29 | * *Active*: Tells if the rule is enabled and active. 30 | * *Remember*: (experimental) When enabled, automatically saves the target monitor when a window that matches to rule was re-position manually by the user. 31 | * *Name*: The rule name 32 | * *URL*: The address that matches the rule. Its case sensitive. (should usually not change). 33 | * *Monitor*: Target-Monitor of the rule. It lists all the available monitors. 34 | * *Default Monitor*: Will pre-select the target-monitor when importing a template (in case no matching monitor was given from the Rules template.) 35 | * *Position*: The position within the *target-monitor* where the window will be placed. 36 | * *Popup*: Shows the web address 37 | 7. Click **UPDATE** to accept the dialog changes. 38 | 8. Finally, click **SAVE** to save the all changes. 39 | 40 | # Development 41 | 42 | ## Getting Started 43 | 44 | ```sh 45 | # Install dependencies 46 | npm install 47 | npm -g install bower 48 | bower install 49 | 50 | # Transform updated source written by ES2015 (default option) 51 | / 52 | 53 | # or Using watch to update source continuously 54 | gulp watch 55 | 56 | # Make a production version extension 57 | gulp build 58 | ``` 59 | 60 | ## Test Chrome Extension 61 | 62 | To test, go to: chrome://extensions, enable Developer mode and load app as an unpacked extension. 63 | 64 | Need more information about Chrome Extension? Please visit [Google Chrome Extension Development](http://developer.chrome.com/extensions/devguide.html) 65 | 66 | ## gulp tasks 67 | 68 | ### Babel 69 | 70 | The generator supports ES 2015 syntax through babel transforming. You may have a source files in `script.babel` if your project has been generated without `--no-babel` options. While developing, When those of source has been changed, `gulp babel` should be run before test and run a extension on Chrome. 71 | 72 | ```sh 73 | gulp babel 74 | ``` 75 | 76 | If you would like to have a continuous transforming by babel you can use `watch` task 77 | 78 | ### Watch 79 | 80 | Watch task helps you reduce your efforts during development extensions. If the task detects your changes of source files, re-compile your sources automatically or Livereload([chromereload.js](https://github.com/yeoman/generator-chrome-extension/blob/master/app/templates/scripts/chromereload.js)) reloads your extension. If you would like to know more about Live-reload and preview of Yeoman? Please see [Getting started with Yeoman and generator-webapp](http://youtu.be/zBt2g9ekiug?t=3m51s) for your understanding. 81 | 82 | ```bash 83 | gulp watch 84 | ``` 85 | 86 | ### Build and Package 87 | 88 | It will build your app as a result you can have a distribution version of the app in `dist`. Run this command to build your Chrome Extension app. 89 | 90 | ```bash 91 | gulp build 92 | ``` 93 | 94 | You can also distribute your project with compressed file using the Chrome Developer Dashboard at Chrome Web Store. This command will compress your app built by `gulp build` command. 95 | 96 | ```bash 97 | gulp package 98 | ``` 99 | 100 | ### ES2015 and babel 101 | 102 | You can use es2015 now for developing the Chrome extension. However, at this moment, you need to execute `babel` task of gulp to compile to test and run your extension on Chrome, because [ES2015 is not full functionality on Chrome as yet](http://kangax.github.io/compat-table/es6/). 103 | 104 | The sources written by es2015 is located at `scripts.babel` and runnable sources are will be at `script` after compiling by `gulp babel`. May you don't want to use babel and ES2015 use `--no-babel` option when scaffolding a new project. 105 | 106 | ## Contribute 107 | 108 | See the [contributing docs](https://github.com/ControlExpert/chrome-multiwindow-positioner/blob/master/contributing.md) 109 | 110 | ## License 111 | 112 | [MIT license](https://github.com/ControlExpert/chrome-multiwindow-positioner/blob/master/LICENSE) 113 | -------------------------------------------------------------------------------- /app/_locales/de/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "appName": { 3 | "message": "MultiWindow Positioner", 4 | "description": "" 5 | }, 6 | "appDescription": { 7 | "message": "Tool-Erweiterung die eine effektive Fensterpositionierung / Platzierung in Multi-Monitor Umgebung ermöglicht.", 8 | "description": "" 9 | }, 10 | "OPTIONS_TITLE": { 11 | "message": "MultiWindow Positioner - Optionen", 12 | "description": "" 13 | }, 14 | "TAB_SETTINGS": { 15 | "message": "Regeln", 16 | "description": "" 17 | }, 18 | "TAB_POSITIONS": { 19 | "message": "Benutzerdefinierte Positionen", 20 | "description": "" 21 | }, 22 | "CUSTOM": { 23 | "message": "Benutzerdefinierte", 24 | "description": "" 25 | }, 26 | "WIDTH": { 27 | "message": "Lange", 28 | "description": "" 29 | }, 30 | "HEIGHT": { 31 | "message": "Hohe", 32 | "description": "" 33 | }, 34 | "ACTIVE": { 35 | "message": "Aktiv", 36 | "description": "" 37 | }, 38 | "NAME": { 39 | "message": "Name", 40 | "description": "" 41 | }, 42 | "URL": { 43 | "message": "URL", 44 | "description": "" 45 | }, 46 | "REMEMBER": { 47 | "message": "Merken", 48 | "description": "" 49 | }, 50 | "MONITOR": { 51 | "message": "Monitor", 52 | "description": "" 53 | }, 54 | "POSITION": { 55 | "message": "Position", 56 | "description": "" 57 | }, 58 | "PLAIN": { 59 | "message": "Ebene", 60 | "description": "" 61 | }, 62 | "EDIT_TAB_RULE": { 63 | "message": "Regel bearbeiten", 64 | "description": "" 65 | }, 66 | "DELETE_TAB_RULE": { 67 | "message": "Regel löschen", 68 | "description": "" 69 | }, 70 | "MOVE_UP": { 71 | "message": "Nach oben", 72 | "description": "" 73 | }, 74 | "MOVE_DOWN": { 75 | "message": "Nach unten", 76 | "description": "" 77 | }, 78 | "NEW_TAB_OPTION_TITLE": { 79 | "message": "Neuer Tab", 80 | "description": "" 81 | }, 82 | "EDIT_TAB_OPTION_TITLE": { 83 | "message": "Tab-Regel bearbeiten", 84 | "description": "" 85 | }, 86 | "TEMPLATE": { 87 | "message": "Vorlage", 88 | "description": "" 89 | }, 90 | "PLAIN_WINDOW": { 91 | "message": "Ebene Fenster", 92 | "description": "" 93 | }, 94 | "ADD": { 95 | "message": "Hinzufügen", 96 | "description": "" 97 | }, 98 | "UPDATE": { 99 | "message": "Aktualisieren", 100 | "description": "" 101 | }, 102 | "CANCEL": { 103 | "message": "Abbrechen", 104 | "description": "" 105 | }, 106 | "IMPORT_TEMPLATE": { 107 | "message": "Vorlage importieren", 108 | "description": "" 109 | }, 110 | "TEMPLATE_URL": { 111 | "message": "Vorlagen-URL", 112 | "description": "" 113 | }, 114 | "REPLACE_ALL_TEMPLATES": { 115 | "message": "Ersetzen Sie alle Vorlage", 116 | "description": "" 117 | }, 118 | "ADD_TAB_OPTION": { 119 | "message": "Regel hinzufügen", 120 | "description": "" 121 | }, 122 | "SAVE": { 123 | "message": "Speichern", 124 | "description": "" 125 | }, 126 | "UNDO": { 127 | "message": "Rücksetzen", 128 | "description": "" 129 | }, 130 | "RELOAD": { 131 | "message": "Neu laden", 132 | "description": "" 133 | }, 134 | "EXPORT_TEMPLATE": { 135 | "message": "Exportvorlage", 136 | "description": "" 137 | }, 138 | "SHOW_MORE_OPTIONS": { 139 | "message": "Weitere Optionen anzeigen", 140 | "description": "" 141 | }, 142 | "VALIDATE_RULES": { 143 | "message": "Überprüfen alle existierende Regel", 144 | "description": "" 145 | }, 146 | "DETECT_MONITORS": { 147 | "message": "Monitore erkennen", 148 | "description": "" 149 | }, 150 | "AUTO_REPAIR_RULES": { 151 | "message": "Reparieren", 152 | "description": "" 153 | }, 154 | "MAXIMIZED": { 155 | "message": "Maximiert", 156 | "description": "" 157 | }, 158 | "LEFT_HALF": { 159 | "message": "linke Hälfte", 160 | "description": "" 161 | }, 162 | "RIGHT_HALF": { 163 | "message": "rechte Hälfte", 164 | "description": "" 165 | }, 166 | "TOP_HALF": { 167 | "message": "obere Hälfte", 168 | "description": "" 169 | }, 170 | "BOTTOM_HALF": { 171 | "message": "untere Hälfte", 172 | "description": "" 173 | }, 174 | "FULLSCREEN": { 175 | "message": "Vollbild", 176 | "description": "" 177 | }, 178 | "DEFAULT_MONITOR": { 179 | "message": "Standardmonitor", 180 | "description": "" 181 | }, 182 | "MAIN_MONITOR": { 183 | "message": "Hauptmonitor", 184 | "description": "" 185 | }, 186 | "NOT_MAIN_MONITOR": { 187 | "message": "Nicht Hauptmonitor", 188 | "description": "" 189 | }, 190 | "BIGGEST_RESOLUTION": { 191 | "message": "Größte Auflösung", 192 | "description": "" 193 | }, 194 | "BIGGEST_HEIGHT": { 195 | "message": "Größte Höhe", 196 | "description": "" 197 | }, 198 | "BIGGEST_WIDTH": { 199 | "message": "Größte Breite", 200 | "description": "" 201 | }, 202 | "SMALLEST_RESOLUTION": { 203 | "message": "Kleinste Auflösung", 204 | "description": "" 205 | }, 206 | "SMALLEST_HEIGHT": { 207 | "message": "Kleinste Höhe", 208 | "description": "" 209 | }, 210 | "SMALLEST_WIDTH": { 211 | "message": "Kleinste Breite", 212 | "description": "" 213 | }, 214 | "RULE_NAME_PLACEHOLDER": { 215 | "message": "Geben Sie einen Regelnamen ein...", 216 | "description": "" 217 | }, 218 | "DRAFT": { 219 | "message": "Entwurf", 220 | "description": "" 221 | }, 222 | "ADD_POSITION": { 223 | "message": "Fügen Sie eine benutzerdefinierte Position hinzu", 224 | "description": "" 225 | }, 226 | "REMOVE_POSITION": { 227 | "message": "Entfernen Sie die benutzerdefinierte Position", 228 | "description": "" 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /app/_locales/en/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "appName": { 3 | "message": "MultiWindow Positioner", 4 | "description": "The name of the application" 5 | }, 6 | "appDescription": { 7 | "message": "Tool extension that enables effective window positioning/placement in multi-monitor setups", 8 | "description": "The description of the application" 9 | }, 10 | "OPTIONS_TITLE": { 11 | "message": "MultiWindow Positioner - Options", 12 | "description": "" 13 | }, 14 | "TAB_SETTINGS": { 15 | "message": "Rules", 16 | "description": "" 17 | }, 18 | "TAB_POSITIONS": { 19 | "message": "Custom Positions", 20 | "description": "" 21 | }, 22 | "CUSTOM": { 23 | "message": "Custom", 24 | "description": "" 25 | }, 26 | "WIDTH": { 27 | "message": "Width", 28 | "description": "" 29 | }, 30 | "HEIGHT": { 31 | "message": "Height", 32 | "description": "" 33 | }, 34 | "ACTIVE": { 35 | "message": "Active", 36 | "description": "" 37 | }, 38 | "NAME": { 39 | "message": "Name", 40 | "description": "" 41 | }, 42 | "URL": { 43 | "message": "URL", 44 | "description": "" 45 | }, 46 | "REMEMBER": { 47 | "message": "Remember", 48 | "description": "" 49 | }, 50 | "MONITOR": { 51 | "message": "Monitor", 52 | "description": "" 53 | }, 54 | "POSITION": { 55 | "message": "Position", 56 | "description": "" 57 | }, 58 | "PLAIN": { 59 | "message": "Plain", 60 | "description": "" 61 | }, 62 | "EDIT_TAB_RULE": { 63 | "message": "Edit tab rule", 64 | "description": "" 65 | }, 66 | "DELETE_TAB_RULE": { 67 | "message": "Delete tab rule", 68 | "description": "" 69 | }, 70 | "MOVE_UP": { 71 | "message": "Move up", 72 | "description": "" 73 | }, 74 | "MOVE_DOWN": { 75 | "message": "Move down", 76 | "description": "" 77 | }, 78 | "NEW_TAB_OPTION_TITLE": { 79 | "message": "New tab option", 80 | "description": "" 81 | }, 82 | "EDIT_TAB_OPTION_TITLE": { 83 | "message": "Edit tab option", 84 | "description": "" 85 | }, 86 | "TEMPLATE": { 87 | "message": "Template", 88 | "description": "" 89 | }, 90 | "PLAIN_WINDOW": { 91 | "message": "Plain Window", 92 | "description": "" 93 | }, 94 | "ADD": { 95 | "message": "ADD", 96 | "description": "" 97 | }, 98 | "UPDATE": { 99 | "message": "UPDATE", 100 | "description": "" 101 | }, 102 | "CANCEL": { 103 | "message": "Cancel", 104 | "description": "" 105 | }, 106 | "IMPORT_TEMPLATE": { 107 | "message": "Import Template", 108 | "description": "" 109 | }, 110 | "TEMPLATE_URL": { 111 | "message": "Template URL", 112 | "description": "" 113 | }, 114 | "REPLACE_ALL_TEMPLATES": { 115 | "message": "Replace all templates", 116 | "description": "" 117 | }, 118 | "ADD_TAB_OPTION": { 119 | "message": "Add Tab Option", 120 | "description": "" 121 | }, 122 | "SAVE": { 123 | "message": "Save", 124 | "description": "" 125 | }, 126 | "UNDO": { 127 | "message": "Undo", 128 | "description": "" 129 | }, 130 | "RELOAD": { 131 | "message": "Re-load", 132 | "description": "" 133 | }, 134 | "EXPORT_TEMPLATE": { 135 | "message": "Export template", 136 | "description": "" 137 | }, 138 | "SHOW_MORE_OPTIONS": { 139 | "message": "Show more options", 140 | "description": "" 141 | }, 142 | "VALIDATE_RULES": { 143 | "message": "Validate tab rules", 144 | "description": "" 145 | }, 146 | "DETECT_MONITORS": { 147 | "message": "Detect Monitors", 148 | "description": "" 149 | }, 150 | "AUTO_REPAIR_RULES": { 151 | "message": "Auto-repair rules", 152 | "description": "" 153 | }, 154 | "MAXIMIZED": { 155 | "message": "Maximized", 156 | "description": "" 157 | }, 158 | "LEFT_HALF": { 159 | "message": "Left-Half", 160 | "description": "" 161 | }, 162 | "RIGHT_HALF": { 163 | "message": "Right-Half", 164 | "description": "" 165 | }, 166 | "TOP_HALF": { 167 | "message": "Top-Half", 168 | "description": "" 169 | }, 170 | "BOTTOM_HALF": { 171 | "message": "Bottom-Half", 172 | "description": "" 173 | }, 174 | "FULLSCREEN": { 175 | "message": "Fullscreen", 176 | "description": "" 177 | }, 178 | "DEFAULT_MONITOR": { 179 | "message": "Default Monitor", 180 | "description": "" 181 | }, 182 | "MAIN_MONITOR": { 183 | "message": "Main monitor", 184 | "description": "" 185 | }, 186 | "NOT_MAIN_MONITOR": { 187 | "message": "Not main monitor", 188 | "description": "" 189 | }, 190 | "BIGGEST_RESOLUTION": { 191 | "message": "Biggest Resolution", 192 | "description": "" 193 | }, 194 | "BIGGEST_HEIGHT": { 195 | "message": "Biggest Height", 196 | "description": "" 197 | }, 198 | "BIGGEST_WIDTH": { 199 | "message": "Biggest Width", 200 | "description": "" 201 | }, 202 | "SMALLEST_RESOLUTION": { 203 | "message": "Smallest Resolution", 204 | "description": "" 205 | }, 206 | "SMALLEST_HEIGHT": { 207 | "message": "Smallest Height", 208 | "description": "" 209 | }, 210 | "SMALLEST_WIDTH": { 211 | "message": "Smallest Width", 212 | "description": "" 213 | }, 214 | "RULE_NAME_PLACEHOLDER": { 215 | "message": "Type a rule name...", 216 | "description": "" 217 | }, 218 | "DRAFT": { 219 | "message": "Draft", 220 | "description": "" 221 | }, 222 | "ADD_POSITION": { 223 | "message": "Add custom position", 224 | "description": "" 225 | }, 226 | "REMOVE_POSITION": { 227 | "message": "Remove custom position", 228 | "description": "" 229 | } 230 | 231 | 232 | } 233 | -------------------------------------------------------------------------------- /app/images/icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ControlExpert/chrome-multiwindow-positioner/95eabc3c24bfc71361fd3379892a8de6118b4038/app/images/icon-128.png -------------------------------------------------------------------------------- /app/images/icon-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ControlExpert/chrome-multiwindow-positioner/95eabc3c24bfc71361fd3379892a8de6118b4038/app/images/icon-16.png -------------------------------------------------------------------------------- /app/images/icon-19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ControlExpert/chrome-multiwindow-positioner/95eabc3c24bfc71361fd3379892a8de6118b4038/app/images/icon-19.png -------------------------------------------------------------------------------- /app/images/icon-38.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ControlExpert/chrome-multiwindow-positioner/95eabc3c24bfc71361fd3379892a8de6118b4038/app/images/icon-38.png -------------------------------------------------------------------------------- /app/images/options-loader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ControlExpert/chrome-multiwindow-positioner/95eabc3c24bfc71361fd3379892a8de6118b4038/app/images/options-loader.gif -------------------------------------------------------------------------------- /app/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "__MSG_appName__", 4 | "short_name": "MWP", 5 | "version": "1.0.17", 6 | 7 | "default_locale": "en", 8 | "description": "__MSG_appDescription__", 9 | "author": "Igor Lino @ ControlExpert", 10 | "icons": { 11 | "16": "images/icon-16.png", 12 | "128": "images/icon-128.png" 13 | }, 14 | 15 | "homepage_url": "https://github.com/ControlExpert/chrome-multiwindow-positioner", 16 | "background": { 17 | "scripts": [ 18 | "scripts/chromereload.js", 19 | "scripts/background.js" 20 | ] 21 | }, 22 | "permissions": [ 23 | "system.display", 24 | "tabs" 25 | ], 26 | "options_ui": { 27 | "page": "options.html", 28 | "chrome_style": true, 29 | "open_in_tab": true 30 | }, 31 | "content_scripts": [ 32 | { 33 | "matches": [ 34 | "http://*/*", 35 | "https://*/*" 36 | ], 37 | "js": [ 38 | "scripts/contentscript.js" 39 | ], 40 | "run_at": "document_end", 41 | "all_frames": false 42 | } 43 | ], 44 | "externally_connectable": { 45 | "matches": ["*://igorlino.github.io/*"] 46 | }, 47 | "web_accessible_resources": [ 48 | "images/icon-48.png", 49 | "images/options-loader.gif" 50 | ] 51 | } 52 | -------------------------------------------------------------------------------- /app/options.html: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 |
29 | 30 |
31 |
32 |
36 |
37 | 43 |
44 |

45 | 46 | 47 | 48 |  -  {{::locale.OPTIONS_TITLE}} 49 | [{{::locale.DRAFT}}]

50 |
51 |
52 |
53 |
54 | 55 |
{{::locale.TAB_SETTINGS}}
59 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 82 | 93 | 94 | 95 | 106 | 117 | 124 | 125 | 133 | 134 | 158 | 159 | 160 |
{{::locale.ACTIVE}}{{::locale.NAME}}{{::locale.URL}}{{::locale.REMEMBER}}{{::locale.CUSTOM}}{{::locale.MONITOR}}{{::locale.DEFAULT_MONITOR}}{{::locale.POSITION}}{{::locale.PLAIN}}
83 | 86 | 87 | 91 | 92 | {{tabOption.name}}{{tabOption.url}} 96 | 99 | 100 | 104 | 105 | 109 | {{tabOption.custom}} 110 | 116 | 118 | {{tabOption.monitor.idx}} {{tabOption.monitor.name}} 119 | 123 | {{localizeDefaultMonitor(tabOption.defaultMonitor)}} 126 | {{localizePosition(tabOption.position)}} 127 | 132 | 135 | 138 | 139 | 140 | 143 | 144 | 145 | 149 | 150 | 151 | 155 | 156 | 157 |
161 |
162 |
163 |
164 | 165 |
166 |
167 |
168 | 169 |
{{::locale.TAB_POSITIONS}}
172 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 191 | 197 | 203 | 209 | 215 | 221 | 229 | 230 | 231 |
{{::locale.NAME}}XY{{::locale.WIDTH}}{{::locale.HEIGHT}}
192 | 196 | 222 | 225 | 226 | 227 | {{monitor.idx}} 228 |
232 |
233 |
234 |
235 | 236 |
237 |
238 |
240 | 241 |
242 | {{::locale.NEW_TAB_OPTION_TITLE}} 243 | {{::locale.EDIT_TAB_OPTION_TITLE}} 244 |
245 |
246 |
247 |
248 |
249 | 252 |
253 | 259 |
260 |
261 |
262 |
263 |
264 |
265 |
266 |
267 | 270 |
271 | 275 | 276 |
277 |
278 | 279 |
280 |
281 | 284 |
285 | 289 | 290 |
291 |
292 | 293 |
294 |
295 | 298 |
299 | 304 |
305 |
306 | 307 |
308 | 311 |
312 | 317 |
318 |
319 | 320 |
321 | 324 |
325 | 330 | 331 |
332 |
333 | 334 |
335 | 338 |
339 | 344 | 345 |
346 |
347 | 348 | 349 |
350 | 353 |
354 | 359 | 360 |
361 |
362 | 363 |
364 | 367 |
368 | 372 | 373 |
374 |
375 | 376 |
377 |
378 |
379 | 380 | 383 | {{::locale.ADD}} 384 | 385 | 388 | {{::locale.UPDATE}} 389 | 390 | {{::locale.CANCEL}} 391 | 392 |
393 |
394 |
395 |
396 | 397 |
398 |
399 |
400 |
401 |
402 |
403 |
404 | 405 |
406 | {{::locale.IMPORT_TEMPLATE}} 407 |
408 |
409 |
410 | 411 |
412 | 415 |
416 | 421 |
422 |
423 |
424 | 427 |
428 | 432 | 433 |
434 |
435 |
436 |
437 |
438 | 439 |
440 | 444 | {{::locale.ADD}} 445 | 446 | {{::locale.CANCEL}} 447 |
448 |
449 |
450 |
451 |
452 |
453 | 454 |
455 | 456 |
457 | 458 |
459 | 463 | 466 | 469 | 473 |
474 | 475 |
476 | 480 | 483 | 488 |
489 | 490 |
491 | 497 | 504 | 510 | 511 |
512 | 517 | 521 | 533 |
534 | 541 |
542 | 543 |
544 | 545 |
546 |
547 |
548 |
549 |

Quick Info

550 |
551 |
552 |
    553 |
  • 554 | Help and getting started click   555 | 561 |
  • 562 |
  • 563 | You are happy for the extension and would like to give a good review/feedback/rating, then click 564 | 565 | here 566 | 567 |
  • 568 |
  • 569 | Support questions may be raised 570 | 571 | here 572 | 573 |
  • 574 |
575 |
576 |
577 |
578 |
579 | 580 | 581 |
582 |
583 |
584 |
585 |
586 |
590 | 591 |
592 |
593 |

Help - Getting Started

594 |
595 |
596 |
    597 |
  1. 598 | You may, at first, import a rules template 599 |
      600 |
    • 601 | Click the IMPORT TEMPLATE icon 602 |
    • 603 |
    • 604 | Give the following URL: https://cdn.rawgit.com/ControlExpert/chrome-multiwindow-positioner/gh-pages/templates/default-template-options.json 605 |
    • 606 |
    • 607 | Click the button ADD to complete the dialog. 608 |
    • 609 |
    • 610 | Finally click SAVE to save all the changes permanently. 611 |
    • 612 |
    613 |
  2. 614 |
  3. 615 | If you need to to use other monitors for specific rules/websites you may edit/add a rule respectively. 616 |
  4. 617 |
  5. 618 | In the edit or create rule dialog you may: 619 |
      620 |
    • Template: List all available rule-templates.
    • 621 |
    • Active: Tells if the rule is enabled and active.
    • 622 |
    • Remember: (experimental) When enabled, automatically saves the target monitor when a window that matches to rule was re-position manually by the user.
    • 623 |
    • Name: The rule name
    • 624 |
    • URL: The address that matches the rule. Its case sensitive. (should usually not change).
    • 625 |
    • Monitor: Target-Monitor of the rule. It lists all the available monitors.
    • 626 |
    • Default Monitor: Will pre-select the target-monitor when importing a template (in case no matching monitor was given from the Rules template.)
    • 627 |
    • Position: The position within the target-monitor where the window will be placed.
    • 628 |
    • Popup: Shows the web address
    • 629 |
    630 |
  6. 631 |
  7. 632 | Click the button UPDATE to accept the dialog changes. 633 |
  8. 634 |
  9. 635 | Finally, click to save the all changes. 636 |
  10. 637 |
638 |
639 |
640 |
641 |
642 |
643 | 644 |
645 | 646 | 647 | 648 | 649 | 650 | 651 | 652 | 653 | 654 | 655 | 656 | 657 | 658 | 659 | 660 | 661 | 662 | 663 | 664 | 665 | 666 | 667 | 668 | 669 | -------------------------------------------------------------------------------- /app/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |

MultiWindow Positioner

19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /app/scripts.babel/background.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | try { 4 | chrome.runtime.onInstalled.addListener(details => { 5 | console.log('previousVersion', details.previousVersion); 6 | }); 7 | } catch(err) { 8 | (console.error || console.log).call(console, err.stack || err); 9 | } 10 | 11 | function showOptionsPage() { 12 | try { 13 | chrome.runtime.openOptionsPage(); 14 | } catch(err) { 15 | (console.error || console.log).call(console, err.stack || err); 16 | } 17 | } 18 | 19 | //chrome.browserAction.setBadgeText({text: '\'Allo'}); 20 | //chrome.browserAction.onClicked.addListener(showOptionsPage); 21 | 22 | //console.log('\'Allo \'Allo! Event Page for Browser Action'); 23 | 24 | const OPTIONS_KEY = 'TAB_HELPER_OPTIONS'; 25 | 26 | const POSITIONS = { 27 | CENTER: {id: 'center', name: 'center'}, 28 | LEFT_HALF: {id: 'left-half', name: 'left-half'}, 29 | RIGHT_HALF: {id: 'right-half', name: 'right-half'}, 30 | TOP_HALF: {id: 'top-half', name: 'top-half'}, 31 | BOTTOM_HALF: {id: 'bottom-half', name: 'bottom-half'}, 32 | FULLSCREEN: { id: 'fullscreen', name: 'fullscreen' } 33 | }; 34 | 35 | const WINDOW_ID_NONE = -1; 36 | const PIXEL_MONITOR_DETECTION_DELTA = 100; 37 | const WINDOW_CHANGE_DETECTION_INTERVAL = 1000; 38 | const MAX_MOVE_TRIES = 10; 39 | 40 | const WINDOW_CACHE_SIZE = 20; 41 | const windowCache = []; 42 | 43 | const WINDOW_STATES = { 44 | NORMAL: 'normal', 45 | MINIMIZED: 'minimized', 46 | MAXIMIZED: 'maximized', 47 | FULLSCREEN: 'fullscreen', 48 | DOCKED: 'docked' 49 | }; 50 | 51 | const states = { 52 | lastWindowInFocus: WINDOW_ID_NONE, 53 | currentWindowInFocus: WINDOW_ID_NONE, 54 | currentWindowLocationHandler: null 55 | }; 56 | 57 | let displayInfos = []; 58 | 59 | loadDisplayInfos(); 60 | 61 | function loadDisplayInfos() { 62 | try { 63 | chrome.system.display.getInfo(function (displayInfosResult) { 64 | displayInfos = displayInfosResult; 65 | }); 66 | } catch(err) { 67 | (console.error || console.log).call(console, err.stack || err); 68 | } 69 | } 70 | 71 | 72 | // chrome.windows.onRemoved.addListener(function callback(windowId) { 73 | // console.log('Window removed ' + windowId); 74 | // const indexToRemove = findCachedWindow(windowId); 75 | // if (indexToRemove !== -1) { 76 | // const window = windowCache[indexToRemove]; 77 | // windowCache.splice(indexToRemove, 1); 78 | // updateTabRules(windowId, window); 79 | // } 80 | // }); 81 | 82 | function findCachedWindow(windowId) { 83 | let found = -1; 84 | try { 85 | for (let idx = 0; idx < windowCache.length; idx++) { 86 | if (windowCache[idx].id === windowId) { 87 | found = idx; 88 | } 89 | } 90 | } catch(err) { 91 | (console.error || console.log).call(console, err.stack || err); 92 | } 93 | return found 94 | } 95 | 96 | function storeWindowIntoCache(window) { 97 | try { 98 | const idx = findCachedWindow(window.id); 99 | if (idx >= 0) { 100 | windowCache.splice(idx, 1); 101 | } 102 | if (windowCache.length >= WINDOW_CACHE_SIZE) { 103 | windowCache.shift(); 104 | } 105 | console.log('Window cached ' + window.id); 106 | windowCache.push(window); 107 | } catch(err) { 108 | (console.error || console.log).call(console, err.stack || err); 109 | } 110 | } 111 | 112 | try { 113 | chrome.windows.onFocusChanged.addListener(onFocusChangeListener); 114 | } catch(err) { 115 | (console.error || console.log).call(console, err.stack || err); 116 | } 117 | 118 | function onFocusChangeListener(windowId) { 119 | try { 120 | console.log('Window Focused ' + windowId); 121 | const allIdentifiersMap = {}; 122 | allIdentifiersMap['i' + states.lastWindowInFocus] = states.lastWindowInFocus; 123 | allIdentifiersMap['i' + states.currentWindowInFocus] = states.currentWindowInFocus; 124 | allIdentifiersMap['i' + windowId] = windowId; 125 | 126 | states.lastWindowInFocus = states.currentWindowInFocus; 127 | states.currentWindowInFocus = windowId; 128 | console.log('Window transition ' + states.lastWindowInFocus + ' to ' + states.currentWindowInFocus); 129 | 130 | for (const key in allIdentifiersMap) { 131 | if (allIdentifiersMap.hasOwnProperty(key)) { 132 | const windowId = allIdentifiersMap[key]; 133 | if (windowId !== WINDOW_ID_NONE) { 134 | startUpdateTabRules(windowId); 135 | } 136 | } 137 | } 138 | } catch(err) { 139 | (console.error || console.log).call(console, err.stack || err); 140 | } 141 | 142 | function startUpdateTabRules(targetWindowId) { 143 | setTimeout(function () { 144 | updateTabRules(targetWindowId); 145 | setTimeout(function () { 146 | updateTabRules(targetWindowId); 147 | }, WINDOW_CHANGE_DETECTION_INTERVAL * 5); 148 | }, WINDOW_CHANGE_DETECTION_INTERVAL); 149 | } 150 | 151 | } 152 | 153 | function updateTabRules(windowId, cachedWindow) { 154 | try { 155 | if (cachedWindow) { 156 | doUpdateTabRules(cachedWindow); 157 | } else { 158 | chrome.windows.get(windowId, { 159 | populate: true 160 | }, function (window) { 161 | try { 162 | if (window) { 163 | storeWindowIntoCache(window); 164 | doUpdateTabRules(window); 165 | } 166 | } catch (e) { 167 | if (e.toString().indexOf('No window with id') >= 0) { 168 | } 169 | } 170 | }); 171 | } 172 | } catch(err) { 173 | (console.error || console.log).call(console, err.stack || err); 174 | } 175 | 176 | function doUpdateTabRules(window) { 177 | if (window && window.tabs) { 178 | const tabRuleOptions = loadOptions(); 179 | for (let idx = 0; idx < window.tabs.length; idx++) { 180 | const tab = window.tabs[idx]; 181 | const tabRule = findTabRuleMatch(tabRuleOptions, tab); 182 | if (tabRule && tabRule.remember && !validateTabLocation(window, tab, tabRule)) { 183 | const monitor = findMonitorByWindow(window); 184 | if (monitor) { 185 | const position = determinePositionByCurrentLocation(monitor, window); 186 | if (position) { 187 | const changed = updateTabRuleByLocation(tabRule, monitor, position, windowId); 188 | if (changed) { 189 | saveOptions(tabRuleOptions); 190 | } 191 | } 192 | } 193 | } 194 | } 195 | } 196 | } 197 | } 198 | 199 | function determinePositionByCurrentLocation(monitor, window) { 200 | let position = POSITIONS.CENTER.id; 201 | try { 202 | if (window.state === WINDOW_STATES.MAXIMIZED) { 203 | position = POSITIONS.CENTER.id; 204 | } else if (window.state === WINDOW_STATES.FULLSCREEN) { 205 | position = POSITIONS.FULLSCREEN.id; 206 | } else { 207 | for (const key in POSITIONS) { 208 | if (POSITIONS.hasOwnProperty(key)) { 209 | const workArea = calculateWorkAreaByPosition(monitor.workArea, POSITIONS[key].id); 210 | if (matchesWorkArea(window, workArea, PIXEL_MONITOR_DETECTION_DELTA)) { 211 | position = POSITIONS[key].id; 212 | break; 213 | } 214 | } 215 | } 216 | } 217 | } catch(err) { 218 | (console.error || console.log).call(console, err.stack || err); 219 | } 220 | return position; 221 | } 222 | 223 | function matchesWorkArea(window, workArea, pixelErrorMargin) { 224 | let matches = false; 225 | try { 226 | const delta = pixelErrorMargin ? pixelErrorMargin : 0; 227 | matches = ( 228 | window.top >= (workArea.top - delta) && 229 | window.top <= (workArea.top + delta) && 230 | window.top + window.height >= (workArea.top - delta) + workArea.height && 231 | window.top + window.height <= (workArea.top + delta) + workArea.height && 232 | window.left >= (workArea.left - delta) && 233 | window.left <= (workArea.left + delta) && 234 | window.left + window.width >= (workArea.left - delta) + workArea.width && 235 | window.left + window.width <= (workArea.left + delta) + workArea.width 236 | ); 237 | } catch(err) { 238 | (console.error || console.log).call(console, err.stack || err); 239 | } 240 | return matches; 241 | } 242 | 243 | function findMonitorByWindow(window) { 244 | let monitor = null; 245 | try { 246 | let highestIdx = -1; 247 | let highestArea = -1; 248 | for (let idx = 0; idx < displayInfos.length; idx++) { 249 | const display = displayInfos[idx]; 250 | const displayWorkArea = display.workArea; 251 | const rightMostLeft = window.left > displayWorkArea.left ? window.left : displayWorkArea.left; 252 | const leftMostRight = window.left + window.width < displayWorkArea.left + displayWorkArea.width ? 253 | window.left + window.width : displayWorkArea.left + displayWorkArea.width; 254 | const bottomMostTop = window.top > displayWorkArea.top ? window.top : displayWorkArea.top; 255 | const topMostBottom = window.top + window.height < displayWorkArea.top + displayWorkArea.height ? 256 | window.top + window.height : displayWorkArea.top + displayWorkArea.height; 257 | 258 | const area = (leftMostRight - rightMostLeft) * (topMostBottom - bottomMostTop); 259 | if (area > highestArea) { 260 | highestArea = area; 261 | highestIdx = idx; 262 | } 263 | /*if (window.top >= displayWorkArea.top && 264 | window.top <= displayWorkArea.top + displayWorkArea.height && 265 | window.left >= displayWorkArea.left && 266 | window.left <= displayWorkArea.left + displayWorkArea.width) { 267 | monitor = display; 268 | break; 269 | }*/ 270 | } 271 | if (highestIdx !== -1) { 272 | monitor = displayInfos[highestIdx]; 273 | } 274 | } catch(err) { 275 | (console.error || console.log).call(console, err.stack || err); 276 | } 277 | return monitor; 278 | } 279 | 280 | function updateTabRuleByLocation(tabRule, monitor, position, windowId) { 281 | let changed = false; 282 | try { 283 | if (tabRule.position !== position && 284 | tabRule.monitor.id !== monitor.id) { 285 | console.log('TabRule Reposition Saved (triggered by window.id:' + windowId + ')'); 286 | console.log(tabRule.position + ' -> ' + position); 287 | console.log(tabRule.monitor.workArea); 288 | console.log(monitor.workArea); 289 | tabRule.position = position; 290 | tabRule.monitor = monitor; 291 | changed = true; 292 | } 293 | } catch(err) { 294 | (console.error || console.log).call(console, err.stack || err); 295 | } 296 | 297 | return changed; 298 | } 299 | 300 | function validateTabLocation(window, tab, tabRule) { 301 | let valid = true; 302 | try { 303 | valid = (window.left === tabRule.monitor.workArea.left && 304 | window.top === tabRule.monitor.workArea.top && 305 | window.width === tabRule.monitor.workArea.width && 306 | window.height === tabRule.monitor.workArea.height); 307 | } catch(err) { 308 | (console.error || console.log).call(console, err.stack || err); 309 | } 310 | return valid; 311 | } 312 | 313 | function findTabRuleMatch(tabRuleOptions, tab) { 314 | let match = null; 315 | try { 316 | if (tab) { 317 | for (let idx = 0; idx < tabRuleOptions.tabs.length; idx++) { 318 | const tabRule = tabRuleOptions.tabs[idx]; 319 | if (tabRule.active && tab.url && tabRule.url && tab.url.indexOf(tabRule.url) >= 0) { 320 | match = tabRule; 321 | break; 322 | } 323 | } 324 | } 325 | } catch(err) { 326 | (console.error || console.log).call(console, err.stack || err); 327 | } 328 | return match; 329 | } 330 | 331 | function findCustomPositionMatch(tabRuleOptions, custom) { 332 | let match = null; 333 | try { 334 | if (custom) { 335 | for (let idx = 0; idx < tabRuleOptions.positions.length; idx++) { 336 | const customPosition = tabRuleOptions.positions[idx]; 337 | if (customPosition.name && customPosition.name !== '' && customPosition.name === custom) { 338 | match = customPosition; 339 | break; 340 | } 341 | } 342 | } 343 | } catch(err) { 344 | (console.error || console.log).call(console, err.stack || err); 345 | } 346 | return match; 347 | } 348 | 349 | function calculateWorkAreaByPosition(monitorWorkArea, position) { 350 | const workarea = { 351 | left: monitorWorkArea.left, 352 | top: monitorWorkArea.top, 353 | width: monitorWorkArea.width, 354 | height: monitorWorkArea.height 355 | }; 356 | 357 | if (position === POSITIONS.LEFT_HALF.id) { 358 | workarea.width = Math.floor(workarea.width / 2); 359 | } 360 | if (position === POSITIONS.RIGHT_HALF.id) { 361 | const halfWidth = Math.floor(workarea.width / 2); 362 | workarea.left += workarea.width - halfWidth; 363 | workarea.width = halfWidth; 364 | } 365 | if (position === POSITIONS.TOP_HALF.id) { 366 | workarea.height = Math.floor(workarea.height / 2); 367 | } 368 | if (position === POSITIONS.BOTTOM_HALF.id) { 369 | const halfHeight = Math.floor(workarea.height / 2); 370 | workarea.top += workarea.height - halfHeight; 371 | workarea.height = halfHeight; 372 | } 373 | return workarea; 374 | } 375 | 376 | function loadOptions() { 377 | let tabRuleOptions = localStorage[OPTIONS_KEY]; 378 | try { 379 | tabRuleOptions = tabRuleOptions ? JSON.parse(tabRuleOptions) : { 380 | tabs: [] 381 | }; 382 | if (!tabRuleOptions.options) { 383 | tabRuleOptions.options = []; 384 | } 385 | } catch(err) { 386 | (console.error || console.log).call(console, err.stack || err); 387 | } 388 | return tabRuleOptions; 389 | } 390 | 391 | function saveOptions(tabRuleOptions) { 392 | localStorage[OPTIONS_KEY] = JSON.stringify(tabRuleOptions); 393 | } 394 | 395 | try { 396 | chrome.tabs.onCreated.addListener(onTabCreated); 397 | //chrome.tabs.onUpdated.addListener(onTabUpdate); 398 | } catch(err) { 399 | (console.error || console.log).call(console, err.stack || err); 400 | } 401 | 402 | // function onTabUpdate(tabId, changeInfo, tab) { 403 | // if (changeInfo.url && changeInfo.url !== '') { 404 | // console.log('Tab updated id:' + tab.id + ' url:' + changeInfo.url); 405 | // onTabCreated(tab, true); 406 | // } 407 | // } 408 | 409 | function getInt(value) { 410 | let intValue = 0; 411 | try { 412 | if (typeof(value) === 'string') { 413 | intValue = parseInt(value, 10); 414 | } else if (typeof(value) === 'number') { 415 | intValue = value; 416 | } 417 | } catch(err) { 418 | (console.error || console.log).call(console, err.stack || err); 419 | } 420 | return intValue; 421 | } 422 | 423 | function onTabCreated(tab, disableCreationMessage) { 424 | try { 425 | if (!disableCreationMessage) { 426 | console.log('Tab Created id:' + tab.id + ' url:' + tab.url); 427 | } 428 | moveTabIntoPositionedWindow(tab, 0); 429 | } catch(err) { 430 | (console.error || console.log).call(console, err.stack || err); 431 | } 432 | 433 | function moveTabIntoPositionedWindow(tab, count) { 434 | if (count > MAX_MOVE_TRIES) { 435 | console.log('Tab with empty url could not be resolved after ' + MAX_MOVE_TRIES + ' tries'); 436 | } 437 | if (!tab.url || tab.url === '') { 438 | console.log('Tab with empty url, trying in 100ms'); 439 | setTimeout(function () { 440 | chrome.tabs.get(tab.id, function (tab) { 441 | moveTabIntoPositionedWindow(tab, count + 1); 442 | }); 443 | }, 100); 444 | } else { 445 | 446 | const tabRuleOptions = loadOptions(); 447 | const tabRule = findTabRuleMatch(tabRuleOptions, tab); 448 | let isCustomPosition = false; 449 | if (tabRule) { 450 | console.log('Tab matched ' + tab.id + ' moving tab with url:' + tab.url); 451 | let createData = calculateWorkAreaByPosition(tabRule.monitor.workArea, tabRule.position); 452 | 453 | if (tabRule.custom && tabRuleOptions.positions && tabRuleOptions.positions.length > 0) { 454 | const customPosition = findCustomPositionMatch(tabRuleOptions, tabRule.custom); 455 | if (customPosition) { 456 | createData = { 457 | left: getInt(customPosition.x), 458 | top: getInt(customPosition.y), 459 | width: getInt(customPosition.width), 460 | height: getInt(customPosition.height) 461 | }; 462 | isCustomPosition = true; 463 | } 464 | } 465 | 466 | createData.tabId = tab.id; 467 | if (tabRule.popup) { 468 | createData.type = 'popup'; 469 | } 470 | chrome.windows.create(createData, function onCreated(window) { 471 | if (!isCustomPosition && tabRule.position === POSITIONS.CENTER.id) { 472 | console.log('Maximizing tab matched ' + tab.id + ' moving tab with url:' + tab.url); 473 | // maximized mode, should only be set after the tab has moved to the right monitor. 474 | chrome.windows.update(window.id, {state:'maximized'}, function onUpdated() { 475 | console.log('Maximized'); 476 | }); 477 | } else if (!isCustomPosition && tabRule.position === POSITIONS.FULLSCREEN.id) { 478 | console.log('Fullscreen tab matched ' + tab.id + ' moving tab with url:' + tab.url); 479 | // maximized mode, should only be set after the tab has moved to the right monitor. 480 | chrome.windows.update(window.id, {state:'fullscreen'}, function onUpdated() { 481 | console.log('Fullscreen'); 482 | }); 483 | } 484 | }); 485 | } 486 | } 487 | } 488 | } 489 | 490 | -------------------------------------------------------------------------------- /app/scripts.babel/chromereload.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Reload client for Chrome Apps & Extensions. 4 | // The reload client has a compatibility with livereload. 5 | // WARNING: only supports reload command. 6 | 7 | try { 8 | const LIVERELOAD_HOST = 'localhost:'; 9 | const LIVERELOAD_PORT = 35729; 10 | const connection = new WebSocket('ws://' + LIVERELOAD_HOST + LIVERELOAD_PORT + '/livereload'); 11 | 12 | connection.onerror = error => { 13 | console.log('reload connection got error:', error); 14 | }; 15 | 16 | connection.onmessage = e => { 17 | try { 18 | if (e.data) { 19 | const data = JSON.parse(e.data); 20 | if (data && data.command === 'reload') { 21 | chrome.runtime.reload(); 22 | } 23 | } 24 | } catch(err) { 25 | (console.error || console.log).call(console, err.stack || err); 26 | } 27 | }; 28 | 29 | } catch(err) { 30 | (console.error || console.log).call(console, err.stack || err); 31 | } -------------------------------------------------------------------------------- /app/scripts.babel/contentscript.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | //console.log('\'Allo \'Allo! Content script'); 4 | -------------------------------------------------------------------------------- /app/scripts.babel/options.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('multiWindowPositioner', ['ngFileUpload', 'ui.checkbox', 'uuid4']).controller('PositionerOptionsController', 4 | ['$scope', '$timeout', 'Upload', '$http', 'uuid4', function ($scope, $timeout, Upload, $http, uuid4) { 5 | const vm = $scope; 6 | 7 | const OPTIONS_KEY = 'TAB_HELPER_OPTIONS'; 8 | const TAB_HELPER_TEMPLATE_URL = 'TAB_HELPER_TEMPLATE_URL'; 9 | 10 | const PAGE_LOADING_OFFSET = 1100; 11 | const PAGE_DETECTION_DISPLAY_INTERVAL = 3;//3 seconds 12 | const MONITORS = {}; 13 | 14 | vm.locale = prepareLocale(); 15 | 16 | const POSITIONS = { 17 | CENTER: {id: 'center', name: vm.locale.MAXIMIZED}, 18 | LEFT_HALF: {id: 'left-half', name: vm.locale.LEFT_HALF}, 19 | RIGHT_HALF: {id: 'right-half', name: vm.locale.RIGHT_HALF}, 20 | TOP_HALF: {id: 'top-half', name: vm.locale.TOP_HALF}, 21 | BOTTOM_HALF: {id: 'bottom-half', name: vm.locale.BOTTOM_HALF}, 22 | FULLSCREEN: { id: 'fullscreen', name: vm.locale.FULLSCREEN } 23 | }; 24 | 25 | const DEFAULT_MONITORS = { 26 | MAIN_MONITOR: {id: 'main-monitor', name: vm.locale.MAIN_MONITOR}, 27 | NOT_MAIN_MONITOR: {id: 'not-main-monitor', name: vm.locale.NOT_MAIN_MONITOR}, 28 | BIGGEST_RESOLUTION: {id: 'biggest-area', name: vm.locale.BIGGEST_RESOLUTION}, 29 | BIGGEST_HEIGHT: {id: 'biggest-height', name: vm.locale.BIGGEST_HEIGHT}, 30 | BIGGEST_WIDTH: {id: 'biggest-width', name: vm.locale.BIGGEST_WIDTH}, 31 | SMALLEST_RESOLUTION: {id: 'smallest-area', name: vm.locale.SMALLEST_RESOLUTION}, 32 | SMALLEST_HEIGHT: {id: 'smallest-height', name: vm.locale.SMALLEST_HEIGHT}, 33 | SMALLEST_WIDTH: {id: 'smallest-width', name: vm.locale.SMALLEST_WIDTH} 34 | }; 35 | 36 | vm.POSITIONS = POSITIONS; 37 | vm.MONITORS = MONITORS; 38 | vm.DEFAULT_MONITORS = DEFAULT_MONITORS; 39 | 40 | 41 | vm.windowHandlers = {}; 42 | vm.options = null; 43 | 44 | vm.showNewTabOption = false; 45 | vm.showEditTabOption = false; 46 | 47 | vm.showImportTemplateDialog = false; 48 | vm.inconsistentOptions = false; 49 | vm.dirty = false; 50 | vm.showExtraOptions = false; 51 | vm.showsHelp = false; 52 | vm.isopen = true; 53 | vm.showImportTemplateDialog = false; 54 | vm.templateUrl = ''; 55 | vm.replaceAllTemplates = true; 56 | 57 | vm.localizeDefaultMonitor = localizeDefaultMonitor; 58 | vm.localizePosition = localizePosition; 59 | vm.markAsDirty = markAsDirk; 60 | vm.saveOptions = saveOptions; 61 | vm.loadOptions = loadOptions; 62 | vm.undoOptions = loadOptions; 63 | vm.reloadOptions = reloadOptions; 64 | vm.detectMonitors = detectMonitors; 65 | vm.showAdvancedOptions = showAdvancedOptions; 66 | vm.addTabOption = addTabOption; 67 | vm.saveTabOption = saveTabOption; 68 | vm.updateTabOption = updateTabOption; 69 | vm.editTabOption = editTabOption; 70 | vm.useTemplateAsOption = useTemplateAsOption; 71 | 72 | vm.addPosition = addPosition; 73 | vm.setCustomPositionAsMonitor = setCustomPositionAsMonitor; 74 | 75 | vm.applyPositionToAll = applyPositionToAll; 76 | vm.applyMonitorToAll = applyMonitorToAll; 77 | 78 | vm.autofixOptions = autofixOptions; 79 | vm.validateOptions = validateOptions; 80 | 81 | vm.moveOptionUp = moveOptionUp; 82 | vm.moveOptionDown = moveOptionDown; 83 | 84 | vm.importTemplate = importTemplate; 85 | vm.openImportTemplateMenu = openImportTemplateMenu; 86 | vm.acceptImportTemplateMenu = acceptImportTemplateMenu; 87 | vm.cancelImportTemplateMenu = cancelImportTemplateMenu; 88 | vm.exportTemplate = exportTemplate; 89 | 90 | vm.cancelTabOption = cancelTabOption; 91 | 92 | vm.deleteTabOption = deleteTabOption; 93 | 94 | vm.deletePositionOption = deletePositionOption; 95 | 96 | vm.toggleHelp = toggleHelp; 97 | 98 | activate(); 99 | 100 | ////////////////////////////////////////////////////////////////// 101 | 102 | 103 | function activate() { 104 | try { 105 | loadOptions(); 106 | loadDisplayInfos(); 107 | registerPostMessageListener(); 108 | doScrollToElement('top-section'); 109 | } catch(err) { 110 | (console.error || console.log).call(console, err.stack || err); 111 | } 112 | } 113 | 114 | function applyPositionToAll(positionId) { 115 | _.forEach(vm.options.tabs, function (tab, idx) { 116 | tab.position = positionId; 117 | }); 118 | markAsDirk(); 119 | } 120 | 121 | function applyMonitorToAll(displayId) { 122 | const monitor = getDisplayById(displayId); 123 | _.forEach(vm.options.tabs, function (tab, idx) { 124 | tab.monitor = monitor; 125 | }); 126 | markAsDirk(); 127 | } 128 | 129 | function registerPostMessageListener() { 130 | try { 131 | chrome.runtime.onMessageExternal.addListener(onMessageExternalListener); 132 | } catch(err) { 133 | (console.error || console.log).call(console, err.stack || err); 134 | } 135 | function onMessageExternalListener(request, sender, sendResponse) { 136 | try { 137 | if (request.closePageGenerator) { 138 | const groupId = request.closePageGenerator; 139 | for (const key in vm.windowHandlers) { 140 | if (vm.windowHandlers.hasOwnProperty(key)) { 141 | const windowHandler = vm.windowHandlers[key]; 142 | if (windowHandler.groupId === groupId) { 143 | closeWindowByHandler(windowHandler); 144 | } 145 | } 146 | } 147 | } 148 | } catch(err) { 149 | (console.error || console.log).call(console, err.stack || err); 150 | } 151 | } 152 | } 153 | 154 | function closeWindowByHandler(windowHandler) { 155 | try { 156 | if (vm.windowHandlers[windowHandler.uuid]) { 157 | chrome.windows.remove(windowHandler.id, function () { 158 | delete vm.windowHandlers[windowHandler.uuid]; 159 | console.log('Removed window ' + windowHandler.id); 160 | }); 161 | } 162 | } catch(err) { 163 | (console.error || console.log).call(console, err.stack || err); 164 | } 165 | } 166 | 167 | function showAdvancedOptions() { 168 | vm.showExtraOptions = !vm.showExtraOptions; 169 | } 170 | 171 | function loadDisplayInfos() { 172 | chrome.system.display.getInfo(function (displayInfos) { 173 | try { 174 | vm.displayInfos = angular.copy(displayInfos); 175 | console.table(displayInfos); 176 | _.forEach(vm.displayInfos, function (display, idx) { 177 | display.idx = idx + 1; 178 | const monitor = { 179 | id: display.id, 180 | idx: display.idx, 181 | name: display.name, //display.idx + ' ' + display.name, 182 | workArea: display.workArea 183 | }; 184 | MONITORS[monitor.id] = monitor; 185 | }); 186 | $timeout(function () { 187 | validateOptions(); 188 | }); 189 | } catch(err) { 190 | (console.error || console.log).call(console, err.stack || err); 191 | } 192 | }); 193 | } 194 | 195 | function detectMonitors() { 196 | try { 197 | const groupId = uuid4.generate(); 198 | 199 | _.forEach(vm.displayInfos, function (display, idx) { 200 | const detectionUrl = 201 | 'https://igorlino.github.io/page-generator/? ' + 202 | 'title=Monitor%20' + (idx + 1) + 203 | '&type=monitor&id=' + (idx + 1) + 204 | '&groupid=' + groupId + 205 | '&extid=' + chrome.runtime.id + 206 | '&delay=' + PAGE_DETECTION_DISPLAY_INTERVAL; 207 | const createData = { 208 | url: detectionUrl, 209 | left: display.workArea.left, 210 | top: display.workArea.top, 211 | width: display.workArea.width, 212 | height: display.workArea.height, 213 | type: 'popup' 214 | }; 215 | const windowHandler = { 216 | groupId: groupId, 217 | uuid: uuid4.generate(), 218 | handler: null 219 | }; 220 | //close-page-generator' 221 | windowHandler.handler = chrome.windows.create(createData, function onWindowsCreated(window) { 222 | windowHandler.id = window.id; 223 | vm.windowHandlers[windowHandler.uuid] = windowHandler; 224 | console.log('Window ' + window.id + ' created.'); 225 | chrome.windows.update(window.id, {state:'maximized'}, function onUpdated() { 226 | console.log('Maximized detection window'); 227 | }); 228 | setTimeout(function () { 229 | console.log('Removing window ' + window.id); 230 | closeWindowByHandler(windowHandler); 231 | }, (PAGE_DETECTION_DISPLAY_INTERVAL * 1000) + PAGE_LOADING_OFFSET); //+800ms to offset detect page loading 232 | }); 233 | }); 234 | } catch(err) { 235 | (console.error || console.log).call(console, err.stack || err); 236 | } 237 | } 238 | 239 | function getPrimaryDisplay() { 240 | let found = null; 241 | _.forEach(vm.displayInfos, function (display, idx) { 242 | if (display.isPrimary) { 243 | found = display; 244 | return false; 245 | } 246 | }); 247 | return found; 248 | } 249 | 250 | function getDisplayById(id) { 251 | let found = null; 252 | _.forEach(vm.displayInfos, function (display, idx) { 253 | if (display.id === id) { 254 | found = display; 255 | return false; 256 | } 257 | }); 258 | return found; 259 | } 260 | 261 | function deleteTabOption(tabOption) { 262 | _.remove(vm.options.tabs, function (option) { 263 | return option.timestamp === tabOption.timestamp; 264 | }); 265 | markAsDirk(); 266 | } 267 | 268 | function deletePositionOption(positionToDelete) { 269 | _.remove(vm.options.positions, function (position) { 270 | return positionToDelete.name === position.name; 271 | }); 272 | markAsDirk(); 273 | } 274 | 275 | function addTabOption() { 276 | vm.showNewTabOption = true; 277 | vm.newTabOption = createNewOption(); 278 | vm.newTabOption.template = null; 279 | } 280 | 281 | function setCustomPositionAsMonitor(position, monitor) { 282 | position.x = monitor.workArea.left; 283 | position.y = monitor.workArea.top; 284 | position.width = monitor.workArea.width; 285 | position.height = monitor.workArea.height; 286 | } 287 | 288 | function addPosition() { 289 | vm.options.positions.push({ 290 | name: 'CustomPosition' + vm.options.positions.length, 291 | x: 0, 292 | y: 0, 293 | height: 10, 294 | width: 10 295 | }); 296 | markAsDirk(); 297 | } 298 | 299 | function editTabOption(tabOption) { 300 | vm.showEditTabOption = true; 301 | vm.newTabOption = angular.copy(tabOption); 302 | vm.newTabOption.position = findPositionById(tabOption.position); 303 | vm.newTabOption.defaultMonitor = findDefaultMonitorById(tabOption.defaultMonitor); 304 | vm.newTabOption.monitor = getDisplayById(tabOption.monitor.id); 305 | vm.editTabOptionIdx = _.findIndex(vm.options.tabs, tabOption); 306 | vm.newTabOption.template = null; 307 | } 308 | 309 | function useTemplateAsOption() { 310 | if (vm.newTabOption.template) { 311 | vm.newTabOption.name = vm.newTabOption.template.name; 312 | vm.newTabOption.url = vm.newTabOption.template.url; 313 | vm.newTabOption.code = vm.newTabOption.template.code; 314 | vm.newTabOption.active = vm.newTabOption.template.active; 315 | vm.newTabOption.remember = vm.newTabOption.template.remember; 316 | if (vm.newTabOption.template.defaultMonitor) { 317 | vm.newTabOption.defaultMonitor = findDefaultMonitorById(vm.newTabOption.template.defaultMonitor); 318 | } 319 | if (vm.newTabOption.template.position) { 320 | vm.newTabOption.position = findPositionById(vm.newTabOption.template.position); 321 | } 322 | } 323 | } 324 | 325 | function findDefaultMonitorById(defaultMonitorId) { 326 | const key = _.findKey(DEFAULT_MONITORS, function (defaultMonitor) { 327 | return defaultMonitor.id === defaultMonitorId; 328 | }); 329 | return key ? DEFAULT_MONITORS[key] : null; 330 | } 331 | 332 | function findPositionById(positionId) { 333 | const positionKey = _.findKey(POSITIONS, function (position) { 334 | return position.id === positionId; 335 | }); 336 | return positionKey ? POSITIONS[positionKey] : null; 337 | } 338 | 339 | function cancelTabOption() { 340 | vm.showNewTabOption = false; 341 | vm.showEditTabOption = false; 342 | } 343 | 344 | function updateTabOption() { 345 | vm.showEditTabOption = false; 346 | vm.options.tabs[vm.editTabOptionIdx] = { 347 | active: vm.newTabOption.active, 348 | code: vm.newTabOption.code, 349 | remember: vm.newTabOption.remember, 350 | url: vm.newTabOption.url, 351 | name: vm.newTabOption.name, 352 | monitor: vm.newTabOption.monitor, 353 | fullScreen: vm.newTabOption.fullScreen, 354 | popup: vm.newTabOption.popup, 355 | position: vm.newTabOption.position ? vm.newTabOption.position.id : MONITORS.CENTER.id, 356 | defaultMonitor: vm.newTabOption.defaultMonitor ? vm.newTabOption.defaultMonitor.id : DEFAULT_MONITORS.MAIN_MONITOR.id, 357 | timestamp: new Date().toISOString() 358 | }; 359 | vm.editTabOptionIdx = -1; 360 | validateOptions(); 361 | markAsDirk(); 362 | } 363 | 364 | function saveTabOption() { 365 | try { 366 | vm.options.tabs.push({ 367 | active: vm.newTabOption.active, 368 | code: vm.newTabOption.code, 369 | remember: vm.newTabOption.remember, 370 | url: vm.newTabOption.url, 371 | name: vm.newTabOption.name, 372 | monitor: vm.newTabOption.monitor, 373 | fullScreen: vm.newTabOption.fullScreen, 374 | popup: vm.newTabOption.popup, 375 | position: vm.newTabOption.position ? vm.newTabOption.position.id : MONITORS.CENTER.id, 376 | defaultMonitor: vm.newTabOption.defaultMonitor ? vm.newTabOption.defaultMonitor.id : DEFAULT_MONITORS.MAIN_MONITOR.id, 377 | timestamp: new Date().toISOString() 378 | }); 379 | vm.showNewTabOption = false; 380 | validateOptions(); 381 | markAsDirk(); 382 | } catch(err) { 383 | (console.error || console.log).call(console, err.stack || err); 384 | } 385 | } 386 | 387 | function saveOptions() { 388 | localStorage[OPTIONS_KEY] = JSON.stringify(vm.options); 389 | markAsPristine(); 390 | //closeCurrentWindow(); 391 | } 392 | 393 | function closeCurrentWindow() { 394 | chrome.tabs.getCurrent(function (tab) { 395 | chrome.tabs.remove(tab.id, function () { 396 | }); 397 | }); 398 | } 399 | 400 | function markAsDirk() { 401 | vm.dirty = true; 402 | } 403 | 404 | function markAsPristine() { 405 | vm.dirty = false; 406 | } 407 | 408 | function loadOptions() { 409 | try { 410 | const tabRuleOptions = localStorage[OPTIONS_KEY]; 411 | if (tabRuleOptions) { 412 | vm.options = JSON.parse(tabRuleOptions); 413 | if (!vm.options.tabs) { 414 | vm.options.tabs = []; 415 | } 416 | if (!vm.options.positions) { 417 | vm.options.positions = []; 418 | } 419 | markAsPristine(); 420 | } else { 421 | vm.options = { 422 | tabs: [], 423 | positions: [] 424 | }; 425 | markAsDirk(); 426 | } 427 | if (!vm.options.templates || vm.options.templates.length <= 0) { 428 | vm.options.templates = getDefaultTemplates(); 429 | } 430 | } catch(err) { 431 | (console.error || console.log).call(console, err.stack || err); 432 | } 433 | //show help by default if no rule has yet been set 434 | if (!vm.showsHelp && vm.options.tabs.length === 0) { 435 | vm.showsHelp = true; 436 | } 437 | return vm.options; 438 | } 439 | 440 | function reloadOptions() { 441 | loadOptions(); 442 | validateOptions(); 443 | } 444 | 445 | function getMappedDefaultMonitorById(defaultMonitors, defaultMonitorId) { 446 | let found = null; 447 | try { 448 | if (defaultMonitorId) { 449 | for (const key in defaultMonitors) { 450 | if (defaultMonitors.hasOwnProperty(key)) { 451 | const defaultMonitor = defaultMonitors[key]; 452 | if (defaultMonitorId === defaultMonitor.id) { 453 | found = defaultMonitor.monitor; 454 | break; 455 | } 456 | } 457 | } 458 | } 459 | } catch(err) { 460 | (console.error || console.log).call(console, err.stack || err); 461 | } 462 | return found; 463 | } 464 | 465 | function getDefaultMonitorsMapping() { 466 | const defaultMonitors = angular.copy(DEFAULT_MONITORS); 467 | try { 468 | _.forEach(vm.displayInfos, function (display, idx) { 469 | if (display.isEnabled) { 470 | const displayWorkArea = display.workArea; 471 | const area = displayWorkArea.height * displayWorkArea.width; 472 | 473 | if (display.isPrimary) { 474 | defaultMonitors.MAIN_MONITOR.monitor = display; 475 | } 476 | if (!display.isPrimary) { 477 | if (!defaultMonitors.NOT_MAIN_MONITOR.monitor) { 478 | defaultMonitors.NOT_MAIN_MONITOR.monitor = display; 479 | } else { 480 | const notMainResolution = defaultMonitors.NOT_MAIN_MONITOR.monitor.workArea.height * 481 | defaultMonitors.NOT_MAIN_MONITOR.monitor.workArea.width; 482 | if (area > notMainResolution) { 483 | defaultMonitors.NOT_MAIN_MONITOR.monitor = display; 484 | } 485 | } 486 | } 487 | if (!defaultMonitors.BIGGEST_RESOLUTION.monitor) { 488 | defaultMonitors.BIGGEST_RESOLUTION.monitor = display; 489 | } else { 490 | const biggestResolution = defaultMonitors.BIGGEST_RESOLUTION.monitor.workArea.height * 491 | defaultMonitors.BIGGEST_RESOLUTION.monitor.workArea.width; 492 | if (area > biggestResolution) { 493 | defaultMonitors.BIGGEST_RESOLUTION.monitor = display; 494 | } 495 | } 496 | if (!defaultMonitors.BIGGEST_HEIGHT.monitor || 497 | displayWorkArea.height > defaultMonitors.BIGGEST_HEIGHT.monitor.workArea.height) { 498 | defaultMonitors.BIGGEST_HEIGHT.monitor = display; 499 | } 500 | if (!defaultMonitors.BIGGEST_WIDTH.monitor || 501 | displayWorkArea.width > defaultMonitors.BIGGEST_WIDTH.monitor.workArea.width) { 502 | defaultMonitors.BIGGEST_WIDTH.monitor = display; 503 | } 504 | if (!defaultMonitors.SMALLEST_RESOLUTION.monitor) { 505 | defaultMonitors.SMALLEST_RESOLUTION.monitor = display; 506 | } else { 507 | const biggestResolution = defaultMonitors.SMALLEST_RESOLUTION.monitor.workArea.height * 508 | defaultMonitors.SMALLEST_RESOLUTION.monitor.workArea.width; 509 | if (area < biggestResolution) { 510 | defaultMonitors.SMALLEST_RESOLUTION.monitor = display; 511 | } 512 | } 513 | if (!defaultMonitors.SMALLEST_HEIGHT.monitor || 514 | displayWorkArea.height < defaultMonitors.SMALLEST_HEIGHT.monitor.workArea.height) { 515 | defaultMonitors.SMALLEST_HEIGHT.monitor = display; 516 | } 517 | if (!defaultMonitors.SMALLEST_WIDTH.monitor || 518 | displayWorkArea.width < defaultMonitors.SMALLEST_WIDTH.monitor.workArea.width) { 519 | defaultMonitors.SMALLEST_WIDTH.monitor = display; 520 | } 521 | } 522 | }); 523 | } catch(err) { 524 | (console.error || console.log).call(console, err.stack || err); 525 | } 526 | return defaultMonitors; 527 | } 528 | 529 | function validateOptions(useDefaultMonitor) { 530 | try { 531 | const defaultMonitors = getDefaultMonitorsMapping(); 532 | let missing = false; 533 | _.forEach(vm.options.tabs, function (tab, idx) { 534 | //verify custom 535 | tab.inconsistentCustom = false; 536 | if (tab.custom && tab.custom !== '') { 537 | let customMatch = false; 538 | _.forEach(vm.options.positions, function (customPosition, idx) { 539 | if (customPosition.name === tab.custom) { 540 | tab.inconsistentCustom = true; 541 | customMatch = true; 542 | return false; 543 | } 544 | }); 545 | if (!customMatch) { 546 | missing = true; 547 | } 548 | } 549 | 550 | //verify display 551 | let displayMatch = false; 552 | _.forEach(vm.displayInfos, function (display, idx) { 553 | if (display.isEnabled && tab.monitor.id === display.id) { 554 | displayMatch = true; 555 | return false; 556 | } 557 | }); 558 | if (!displayMatch) { 559 | let defaultMonitor = null; 560 | if (useDefaultMonitor) { 561 | defaultMonitor = getMappedDefaultMonitorById(defaultMonitors, tab.defaultMonitor); 562 | if (defaultMonitor) { 563 | tab.monitor = angular.copy(defaultMonitor); 564 | } 565 | } 566 | if (!useDefaultMonitor || !defaultMonitor) { 567 | tab.inconsistentMonitor = true; 568 | missing = true; 569 | } 570 | } else { 571 | tab.inconsistentMonitor = false; 572 | } 573 | }); 574 | vm.inconsistentOptions = missing; 575 | } catch(err) { 576 | (console.error || console.log).call(console, err.stack || err); 577 | } 578 | } 579 | 580 | function autofixOptions() { 581 | try { 582 | const primaryDisplay = getPrimaryDisplay(); 583 | _.forEach(vm.options.tabs, function (tab, idx) { 584 | let found = false; 585 | let closestMatch = null; 586 | _.forEach(vm.displayInfos, function (display, idx) { 587 | if (display.isEnabled && display.workArea && 588 | display.workArea.height === tab.monitor.workArea.height && 589 | display.workArea.width === tab.monitor.workArea.width) { 590 | closestMatch = display; 591 | } 592 | if (tab.monitor.id === display.id) { 593 | found = true; 594 | return false; 595 | } 596 | }); 597 | if (!found) { 598 | if (closestMatch) { 599 | replaceMonitor(tab.monitor, closestMatch); 600 | } else if (primaryDisplay) { 601 | replaceMonitor(tab.monitor, primaryDisplay); 602 | } 603 | //monitor: vm.newTabOption.monitor, 604 | //position: vm.newTabOption.position.id, 605 | } 606 | }); 607 | 608 | validateOptions(); 609 | } catch(err) { 610 | (console.error || console.log).call(console, err.stack || err); 611 | } 612 | 613 | function replaceMonitor(target, sourceDisplay) { 614 | target.workArea = angular.copy(sourceDisplay.workArea); 615 | target.id = sourceDisplay.id; 616 | const idx = _.findIndex(vm.displayInfos, sourceDisplay); 617 | target.name = sourceDisplay.name;//(idx + 1) + ' ' + sourceDisplay.name; 618 | target.idx = idx + 1; 619 | } 620 | } 621 | 622 | function createNewOption() { 623 | return { 624 | active: true, 625 | remember: false, 626 | code: 'custom', 627 | name: vm.locale.RULE_NAME_PLACEHOLDER, 628 | url: 'http://any.url/', 629 | monitor: getPrimaryDisplay(), 630 | defaultMonitor: DEFAULT_MONITORS.MAIN_MONITOR, 631 | fullScreen: false, 632 | popup: true, 633 | position: POSITIONS.CENTER 634 | }; 635 | } 636 | 637 | function openImportTemplateMenu() { 638 | const templateUrl = localStorage[TAB_HELPER_TEMPLATE_URL]; 639 | if (templateUrl) { 640 | vm.templateUrl = templateUrl; 641 | } 642 | vm.showImportTemplateDialog = true; 643 | } 644 | 645 | function acceptImportTemplateMenu(templateUrl) { 646 | try { 647 | vm.showImportTemplateDialog = false; 648 | if (templateUrl && templateUrl !== '') { 649 | localStorage[TAB_HELPER_TEMPLATE_URL] = templateUrl; 650 | vm.templateUrl = templateUrl; 651 | 652 | callHttpByGet(templateUrl, function onResponse(response) { 653 | if (response.success && response.data) { 654 | if (response.data.tabs) { 655 | mergeRules(vm.options.tabs, response.data.tabs); 656 | } 657 | if (response.data.templates) { 658 | if (vm.replaceAllTemplates) { 659 | vm.options.templates = response.data.templates; 660 | } else { 661 | mergeTemplates(vm.options.templates, response.data.templates); 662 | } 663 | } 664 | validateOptions(true); 665 | markAsDirk(); 666 | } 667 | }); 668 | } 669 | } catch(err) { 670 | (console.error || console.log).call(console, err.stack || err); 671 | } 672 | } 673 | 674 | function mergeRules(existentRules, templateRules) { 675 | try { 676 | cleanOrder(existentRules); 677 | 678 | for (let i = 0; i < existentRules.length; i++) { 679 | const rule = existentRules[i]; 680 | for (let k = 0; k < templateRules.length; k++) { 681 | const template = templateRules[k]; 682 | template.order = k + 1; 683 | if (rule.code === template.code) { 684 | //mark as merged 685 | template.merged = true; 686 | //rule.active = template.active; 687 | //rule.code = template.code; 688 | //rule.remember = template.remember; 689 | rule.url = template.url; 690 | rule.name = template.name; 691 | //rule.monitor = template.monitor; 692 | rule.defaultMonitor = template.defaultMonitor; 693 | //rule.fullScreen = template.fullScreen; 694 | //rule.popup = template.popup; 695 | //rule.position = template.position ? template.position.id : 'center'; 696 | 697 | rule.order = template.order; 698 | } 699 | } 700 | } 701 | 702 | //add new templates 703 | for (let k = 0; k < templateRules.length; k++) { 704 | const template = templateRules[k]; 705 | if (!template.merged) { 706 | existentRules.push(template); 707 | } 708 | } 709 | 710 | sortByOrder(existentRules); 711 | } catch(err) { 712 | (console.error || console.log).call(console, err.stack || err); 713 | } 714 | } 715 | 716 | //clean any order value. 717 | function cleanOrder(list) { 718 | for (let i = 0; i < list.length; i++) { 719 | delete list[i].order; 720 | } 721 | } 722 | 723 | function sortByOrder(list) { 724 | //sort items by order 725 | for (let i = 0; i < list.length; i++) { 726 | for (let k = 0; k < list.length - 1 - i; k++) { 727 | const item1 = list[k]; 728 | const item2 = list[k + 1]; 729 | 730 | if ((item1.order && item2.order && item1.order > item2.order) || 731 | (item1.order && !item2.order)) { 732 | list[k] = item2; 733 | list[k + 1] = item1; 734 | } 735 | } 736 | } 737 | } 738 | 739 | function mergeTemplates(currentTemplates, newTemplates) { 740 | try { 741 | cleanOrder(currentTemplates); 742 | 743 | for (let i = 0; i < currentTemplates.length; i++) { 744 | const existingTemplate = currentTemplates[i]; 745 | for (let k = 0; k < newTemplates.length; k++) { 746 | const newTemplate = newTemplates[k]; 747 | newTemplate.order = k + 1; 748 | if (existingTemplate.code === newTemplate.code) { 749 | //mark as merged 750 | newTemplate.merged = true; 751 | existingTemplate.active = newTemplate.active; 752 | existingTemplate.code = newTemplate.code; 753 | existingTemplate.remember = newTemplate.remember; 754 | existingTemplate.url = newTemplate.url; 755 | existingTemplate.name = newTemplate.name; 756 | existingTemplate.defaultMonitor = newTemplate.defaultMonitor; 757 | existingTemplate.position = newTemplate.position; 758 | 759 | existingTemplate.order = newTemplate.order; 760 | } 761 | } 762 | } 763 | 764 | //add new templates 765 | for (let k = 0; k < newTemplates.length; k++) { 766 | const template = newTemplates[k]; 767 | if (!newTemplate.merged) { 768 | currentTemplates.push(template); 769 | } 770 | } 771 | 772 | //sort templates by order 773 | sortByOrder(currentTemplates); 774 | } catch(err) { 775 | (console.error || console.log).call(console, err.stack || err); 776 | } 777 | } 778 | 779 | function cancelImportTemplateMenu() { 780 | vm.showImportTemplateDialog = false; 781 | vm.templateUrl = ''; 782 | } 783 | 784 | function importTemplate(file) { 785 | Upload.upload({ 786 | url: 'upload/url', 787 | data: {file: file, 'username': $scope.username} 788 | }).then(function (resp) { 789 | console.log('Success ' + resp.config.data.file.name + 'uploaded. Response: ' + resp.data); 790 | }, function (resp) { 791 | console.log('Error status: ' + resp.status); 792 | }, function (evt) { 793 | const progressPercentage = parseInt(100.0 * evt.loaded / evt.total); 794 | if (evt.config.data.file.name) { 795 | console.log('progress: ' + progressPercentage + '% ' + evt.config.data.file.name); 796 | } else { 797 | console.log('progress: ' + progressPercentage + '% ' + evt.config.data.file); 798 | } 799 | }); 800 | } 801 | 802 | function exportTemplate() { 803 | const optionsAsJson = angular.toJson(vm.options, 3); 804 | const blob = new Blob([optionsAsJson], {type: 'application/json'}); 805 | const saveAs = window.saveAs; 806 | saveAs(blob, 'multiwindow-positioner-rule-export.json'); 807 | } 808 | 809 | function getDefaultTemplates() { 810 | return [ 811 | { 812 | active: true, 813 | remember: false, 814 | name: 'Google Search', 815 | url: 'https://www.google.com/', 816 | code: 'google-search', 817 | defaultMonitor: 'main-monitor' 818 | }, 819 | { 820 | active: true, 821 | remember: false, 822 | name: 'Facebook', 823 | url: 'https://www.facebook.com', 824 | code: 'facebook', 825 | defaultMonitor: 'main-monitor' 826 | }, 827 | { 828 | active: true, 829 | remember: false, 830 | name: 'YouTube', 831 | url: 'https://www.youtube.com/', 832 | code: 'google-youtube', 833 | defaultMonitor: 'main-monitor' 834 | }, 835 | { 836 | active: true, 837 | remember: false, 838 | name: 'Wikipedia', 839 | url: 'https://www.wikipedia.org/', 840 | code: 'wikipedia', 841 | defaultMonitor: 'main-monitor' 842 | }, 843 | { 844 | active: true, 845 | remember: false, 846 | name: 'Amazon', 847 | url: 'https://www.amazon.com/', 848 | code: 'amazon-global', 849 | defaultMonitor: 'main-monitor' 850 | }, 851 | { 852 | active: true, 853 | remember: false, 854 | name: 'Ebay', 855 | url: 'http://www.ebay.com/', 856 | code: 'ebay-global', 857 | defaultMonitor: 'main-monitor' 858 | } 859 | ] 860 | } 861 | 862 | function moveOptionUp(first, index) { 863 | if (!first) { 864 | swap(vm.options.tabs, index, index - 1); 865 | markAsDirk(); 866 | } 867 | } 868 | 869 | function moveOptionDown(last, index) { 870 | if (!last) { 871 | swap(vm.options.tabs, index, index + 1); 872 | markAsDirk(); 873 | } 874 | } 875 | 876 | function swap(list, idx1, idx2) { 877 | const tmp = list[idx1]; 878 | list[idx1] = list[idx2]; 879 | list[idx2] = tmp; 880 | } 881 | 882 | function callHttpByGet(callPath, callback) { 883 | $http({ 884 | method: 'get', 885 | url: callPath, 886 | headers: {'Cache-Control': 'no-cache'} 887 | }).then(onSuccess, onError); 888 | 889 | function onError(response) { 890 | const result = handleHttpError(response.data, callPath); 891 | callback(result); 892 | } 893 | 894 | function onSuccess(response) { 895 | const result = 896 | { 897 | success: true, 898 | data: response.data 899 | }; 900 | 901 | callback(result); 902 | } 903 | } 904 | 905 | function handleHttpError(data, callPath) { 906 | const response = {success: false}; 907 | if (data) { 908 | response.error = data; 909 | } 910 | else { 911 | response.error = dateNow() + ' - Request failed: ' + callPath; 912 | } 913 | return response; 914 | } 915 | 916 | function dateNow() { 917 | const d = new Date(); 918 | return d.toLocaleString(); 919 | } 920 | 921 | function prepareLocale() { 922 | return { 923 | OPTIONS_TITLE: chrome.i18n.getMessage('OPTIONS_TITLE'), 924 | TAB_SETTINGS: chrome.i18n.getMessage('TAB_SETTINGS'), 925 | 926 | //custom positions 927 | TAB_POSITIONS: chrome.i18n.getMessage('TAB_POSITIONS'), 928 | CUSTOM: chrome.i18n.getMessage('CUSTOM'), 929 | WIDTH: chrome.i18n.getMessage('WIDTH'), 930 | HEIGHT: chrome.i18n.getMessage('HEIGHT'), 931 | ADD_POSITION: chrome.i18n.getMessage('ADD_POSITION'), 932 | REMOVE_POSITION: chrome.i18n.getMessage('REMOVE_POSITION'), 933 | 934 | //table columns 935 | ACTIVE: chrome.i18n.getMessage('ACTIVE'), 936 | NAME: chrome.i18n.getMessage('NAME'), 937 | URL: chrome.i18n.getMessage('URL'), 938 | REMEMBER: chrome.i18n.getMessage('REMEMBER'), 939 | MONITOR: chrome.i18n.getMessage('MONITOR'), 940 | POSITION: chrome.i18n.getMessage('POSITION'), 941 | PLAIN: chrome.i18n.getMessage('PLAIN'), 942 | 943 | //table actions 944 | EDIT_TAB_RULE: chrome.i18n.getMessage('EDIT_TAB_RULE'), 945 | DELETE_TAB_RULE: chrome.i18n.getMessage('DELETE_TAB_RULE'), 946 | MOVE_UP: chrome.i18n.getMessage('MOVE_UP'), 947 | MOVE_DOWN: chrome.i18n.getMessage('MOVE_DOWN'), 948 | 949 | //dialogs 950 | NEW_TAB_OPTION_TITLE: chrome.i18n.getMessage('NEW_TAB_OPTION_TITLE'), 951 | EDIT_TAB_OPTION_TITLE: chrome.i18n.getMessage('EDIT_TAB_OPTION_TITLE'), 952 | TEMPLATE: chrome.i18n.getMessage('TEMPLATE'), 953 | PLAIN_WINDOW: chrome.i18n.getMessage('PLAIN_WINDOW'), 954 | ADD: chrome.i18n.getMessage('ADD'), 955 | UPDATE: chrome.i18n.getMessage('UPDATE'), 956 | CANCEL: chrome.i18n.getMessage('CANCEL'), 957 | TEMPLATE_URL: chrome.i18n.getMessage('TEMPLATE_URL'), 958 | REPLACE_ALL_TEMPLATES: chrome.i18n.getMessage('REPLACE_ALL_TEMPLATES'), 959 | 960 | //actions 961 | ADD_TAB_OPTION: chrome.i18n.getMessage('ADD_TAB_OPTION'), 962 | SAVE: chrome.i18n.getMessage('SAVE'), 963 | UNDO: chrome.i18n.getMessage('UNDO'), 964 | RELOAD: chrome.i18n.getMessage('RELOAD'), 965 | IMPORT_TEMPLATE: chrome.i18n.getMessage('IMPORT_TEMPLATE'), 966 | EXPORT_TEMPLATE: chrome.i18n.getMessage('EXPORT_TEMPLATE'), 967 | SHOW_MORE_OPTIONS: chrome.i18n.getMessage('SHOW_MORE_OPTIONS'), 968 | VALIDATE_RULES: chrome.i18n.getMessage('VALIDATE_RULES'), 969 | DETECT_MONITORS: chrome.i18n.getMessage('DETECT_MONITORS'), 970 | AUTO_REPAIR_RULES: chrome.i18n.getMessage('AUTO_REPAIR_RULES'), 971 | 972 | //window positions 973 | MAXIMIZED: chrome.i18n.getMessage('MAXIMIZED'), 974 | LEFT_HALF: chrome.i18n.getMessage('LEFT_HALF'), 975 | RIGHT_HALF: chrome.i18n.getMessage('RIGHT_HALF'), 976 | TOP_HALF: chrome.i18n.getMessage('TOP_HALF'), 977 | BOTTOM_HALF: chrome.i18n.getMessage('BOTTOM_HALF'), 978 | FULLSCREEN: chrome.i18n.getMessage('FULLSCREEN'), 979 | 980 | //default monitors 981 | DEFAULT_MONITOR: chrome.i18n.getMessage('DEFAULT_MONITOR'), 982 | MAIN_MONITOR: chrome.i18n.getMessage('MAIN_MONITOR'), 983 | NOT_MAIN_MONITOR: chrome.i18n.getMessage('NOT_MAIN_MONITOR'), 984 | BIGGEST_RESOLUTION: chrome.i18n.getMessage('BIGGEST_RESOLUTION'), 985 | BIGGEST_HEIGHT: chrome.i18n.getMessage('BIGGEST_HEIGHT'), 986 | BIGGEST_WIDTH: chrome.i18n.getMessage('BIGGEST_WIDTH'), 987 | SMALLEST_RESOLUTION: chrome.i18n.getMessage('SMALLEST_RESOLUTION'), 988 | SMALLEST_HEIGHT: chrome.i18n.getMessage('SMALLEST_HEIGHT'), 989 | SMALLEST_WIDTH: chrome.i18n.getMessage('SMALLEST_WIDTH'), 990 | 991 | RULE_NAME_PLACEHOLDER: chrome.i18n.getMessage('RULE_NAME_PLACEHOLDER'), 992 | 993 | DRAFT: chrome.i18n.getMessage('DRAFT') 994 | }; 995 | } 996 | 997 | function localizeDefaultMonitor(defaultMonitor) { 998 | let localizedDefaultMonitor = defaultMonitor; 999 | if (defaultMonitor === DEFAULT_MONITORS.MAIN_MONITOR.id) { 1000 | localizedDefaultMonitor = vm.locale.MAIN_MONITOR; 1001 | } else if (defaultMonitor === DEFAULT_MONITORS.NOT_MAIN_MONITOR.id) { 1002 | localizedDefaultMonitor = vm.locale.NOT_MAIN_MONITOR; 1003 | } else if (defaultMonitor === DEFAULT_MONITORS.BIGGEST_RESOLUTION.id) { 1004 | localizedDefaultMonitor = vm.locale.BIGGEST_RESOLUTION; 1005 | } else if (defaultMonitor === DEFAULT_MONITORS.BIGGEST_HEIGHT.id) { 1006 | localizedDefaultMonitor = vm.locale.BIGGEST_HEIGHT; 1007 | } else if (defaultMonitor === DEFAULT_MONITORS.BIGGEST_WIDTH.id) { 1008 | localizedDefaultMonitor = vm.locale.BIGGEST_WIDTH; 1009 | } else if (defaultMonitor === DEFAULT_MONITORS.SMALLEST_RESOLUTION.id) { 1010 | localizedDefaultMonitor = vm.locale.SMALLEST_RESOLUTION; 1011 | } else if (defaultMonitor === DEFAULT_MONITORS.SMALLEST_HEIGHT.id) { 1012 | localizedDefaultMonitor = vm.locale.SMALLEST_HEIGHT; 1013 | } else if (defaultMonitor === DEFAULT_MONITORS.SMALLEST_WIDTH.id) { 1014 | localizedDefaultMonitor = vm.locale.SMALLEST_WIDTH; 1015 | } 1016 | 1017 | return localizedDefaultMonitor 1018 | } 1019 | 1020 | function localizePosition(position) { 1021 | let localizedPosition = position; 1022 | if (position === POSITIONS.CENTER.id) { 1023 | localizedPosition = vm.locale.MAXIMIZED; 1024 | } else if (position === POSITIONS.LEFT_HALF.id) { 1025 | localizedPosition = vm.locale.LEFT_HALF; 1026 | } else if (position === POSITIONS.RIGHT_HALF.id) { 1027 | localizedPosition = vm.locale.RIGHT_HALF; 1028 | } else if (position === POSITIONS.TOP_HALF.id) { 1029 | localizedPosition = vm.locale.TOP_HALF; 1030 | } else if (position === POSITIONS.BOTTOM_HALF.id) { 1031 | localizedPosition = vm.locale.BOTTOM_HALF; 1032 | } else if (position === POSITIONS.FULLSCREEN.id) { 1033 | localizedPosition = vm.locale.FULLSCREEN; 1034 | } 1035 | 1036 | return localizedPosition 1037 | } 1038 | 1039 | function toggleHelp() { 1040 | vm.showsHelp = !vm.showsHelp; 1041 | if (vm.showsHelp) { 1042 | doScrollToElement('quick-info-section'); 1043 | } else { 1044 | doScrollToElement('top-section'); 1045 | } 1046 | } 1047 | 1048 | function doScrollToElement(elementId, notifyScroll, duration, offset) { 1049 | var defaultDuration = angular.isDefined(duration) ? duration : 0; //milliseconds 1050 | var defaultOffset = angular.isDefined(offset) ? offset : 0; //pixels; adjust for floating menu, context etc 1051 | //Scroll to #some-id with 30 px "padding" 1052 | //Note: Use this in a directive, not with document.getElementById 1053 | 1054 | var container = jQuery('html, body'); 1055 | container.stop(); 1056 | 1057 | var elementToScroll = jQuery('#' + elementId); 1058 | if (elementToScroll && elementToScroll.length > 0) { 1059 | $timeout(function () { 1060 | container.animate({ 1061 | scrollTop: jQuery('#' + elementId).offset().top + defaultOffset 1062 | }, 800); 1063 | }, 100, false); 1064 | } 1065 | } 1066 | 1067 | }]); 1068 | -------------------------------------------------------------------------------- /app/scripts.babel/popup.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | //console.log('\'Allo \'Allo! Popup'); 4 | -------------------------------------------------------------------------------- /app/styles/bootstrap.css.map: -------------------------------------------------------------------------------- 1 | { 2 | version:"1.0" 3 | } -------------------------------------------------------------------------------- /app/styles/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 20px; 3 | } 4 | 5 | .centered { 6 | position: absolute; 7 | top: 50%; 8 | left: 50%; 9 | transform: translate(-50%, -50%) scale(2); 10 | } 11 | 12 | .options-loader.centered { 13 | } 14 | 15 | .options-loader { 16 | background: url(/images/options-loader.gif) no-repeat center center; 17 | width: 200px; 18 | height: 200px; 19 | } 20 | 21 | .wait-in-hidden { 22 | opacity: 0; 23 | transition: opacity 1s; 24 | } 25 | 26 | .wait-in-hidden-done { 27 | opacity: 1; 28 | } 29 | 30 | .wait-in-show { 31 | opacity: 1; 32 | transition: opacity 1s; 33 | } 34 | .wait-in-show-done { 35 | opacity: 0; 36 | } 37 | 38 | .tab-helper-options { 39 | width : 1280px; 40 | margin-left: auto; 41 | margin-right: auto; 42 | } 43 | .tab-helper-options.wider { 44 | width : 1680px; 45 | } 46 | 47 | .missing-monitor { 48 | color: red; 49 | } 50 | 51 | .tab-action-bar .btn { 52 | border: none; 53 | color: lightgray; 54 | } 55 | 56 | tr:hover .tab-action-bar .btn { 57 | color: black; 58 | } 59 | 60 | .hidden-only { 61 | visibility: hidden; 62 | } 63 | 64 | .fix-checkbox { 65 | min-width: inherit !important 66 | } 67 | 68 | .fix-checkbox-mark { 69 | width: 10px; 70 | left: -1px; 71 | padding-left: 5px; 72 | } 73 | 74 | .rule-dialog .form-group { 75 | 76 | } 77 | 78 | .margin-left-lg { 79 | margin-left: 30px !important; 80 | } 81 | 82 | .margin-top-md { 83 | margin-top: 8px !important; 84 | } 85 | 86 | .margin-md { 87 | margin: 8px !important; 88 | } 89 | 90 | .hoverable-section { 91 | 92 | } 93 | 94 | .hoverable-section .show-when-hover { 95 | display: none; 96 | } 97 | 98 | .hoverable-section:hover .show-when-hover { 99 | display: block; 100 | } 101 | 102 | .hoverable-section .hide-when-hover { 103 | display: block; 104 | } 105 | 106 | .hoverable-section:hover .hide-when-hover { 107 | display: none; 108 | } 109 | 110 | .options-rule-row td, 111 | .options-position-row td { 112 | line-height: 34px !important 113 | } 114 | 115 | .custom-select { 116 | max-width: 250px !important; 117 | min-width: 250px !important; 118 | } 119 | 120 | .monitor-select { 121 | max-width: 250px !important; 122 | min-width: 250px !important; 123 | } 124 | 125 | .position-select { 126 | max-width: 120px !important; 127 | min-width: 120px !important; 128 | } 129 | 130 | .checkbox-cell { 131 | padding-top: 14px !important; 132 | } 133 | 134 | .dropdown-header { 135 | margin-bottom:15px; 136 | } 137 | 138 | .help-section { 139 | padding: 8px !important; 140 | border: 9px #2e6da4 solid; 141 | display: inline-block; 142 | width: 100%; 143 | position: relative; 144 | } 145 | 146 | .quick-info-section { 147 | padding: 8px !important; 148 | border: 9px darkgrey solid; 149 | display: inline-block; 150 | width: 100%; 151 | position: relative; 152 | } 153 | 154 | .logo-wrapper { 155 | float: left; 156 | width: 289px; 157 | } 158 | .logo-wrapper > img { 159 | max-width: 274px; 160 | float: left; 161 | margin-right: 12px; 162 | margin-top: -2px; 163 | } 164 | 165 | .help-button-container { 166 | position: fixed; 167 | right: 18px; 168 | top: 40px; 169 | } 170 | 171 | .help-section-close { 172 | position: absolute; 173 | right: 8px; 174 | top: 8px; 175 | color: darkgrey; 176 | cursor:pointer; 177 | z-index: 10; 178 | } 179 | 180 | .help-section-close:hover { 181 | color: inherit; 182 | } -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chrome-multiwindow-positioner", 3 | "private": true, 4 | "version": "1.0.15", 5 | "description": "Tool extension that enables seamless window placement/positioning in multi-monitor setups.", 6 | "authors": [ 7 | "Control Expert" 8 | ], 9 | "license": "MIT", 10 | "dependencies": { 11 | "jquery": "~3.3.1", 12 | "lodash": "4.17.5", 13 | "bootstrap": "3.3.7", 14 | "angular": "~1.6.9", 15 | "angular-resource": "~1.6.9", 16 | "font-awesome": "~4.7.0", 17 | "select2-bootstrap-css": "1.4.6", 18 | "angular-bootstrap": "~2.5.0", 19 | "file-saver": "~1.3.8", 20 | "ng-file-upload": "~12.2.12", 21 | "angular-uuid4": "0.3.1", 22 | "angular-bootstrap-checkbox": "~0.5.1", 23 | "angular-intro.js": "~3.3.0" 24 | }, 25 | "devDependencies": {}, 26 | "overrides": { 27 | "bootstrap": { 28 | "main": [ 29 | "dist/css/bootstrap.css", 30 | "dist/js/bootstrap.js" 31 | ] 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /gulpfile.babel.js: -------------------------------------------------------------------------------- 1 | // generated on 2016-10-04 using generator-chrome-extension 0.6.1 2 | import gulp from 'gulp'; 3 | import gulpLoadPlugins from 'gulp-load-plugins'; 4 | import del from 'del'; 5 | import runSequence from 'run-sequence'; 6 | import {stream as wiredep} from 'wiredep'; 7 | 8 | const $ = gulpLoadPlugins(); 9 | 10 | gulp.task('extras', () => { 11 | return gulp.src([ 12 | 'app/*.*', 13 | 'app/_locales/**', 14 | '!app/scripts.babel', 15 | '!app/*.json', 16 | '!app/*.html', 17 | ], { 18 | base: 'app', 19 | dot: true 20 | }).pipe(gulp.dest('dist')); 21 | }); 22 | 23 | function lint(files, options) { 24 | return () => { 25 | return gulp.src(files) 26 | .pipe($.eslint(options)) 27 | .pipe($.eslint.format()); 28 | }; 29 | } 30 | 31 | gulp.task('lint', lint('app/scripts.babel/**/*.js', { 32 | env: { 33 | es6: true 34 | } 35 | })); 36 | 37 | gulp.task('images', () => { 38 | return gulp.src('app/images/**/*') 39 | .pipe($.if($.if.isFile, $.cache($.imagemin({ 40 | progressive: true, 41 | interlaced: true, 42 | // don't remove IDs from SVGs, they are often used 43 | // as hooks for embedding and styling 44 | svgoPlugins: [{cleanupIDs: false}] 45 | })) 46 | .on('error', function (err) { 47 | console.log(err); 48 | this.end(); 49 | }))) 50 | .pipe(gulp.dest('dist/images')); 51 | }); 52 | 53 | gulp.task('html', () => { 54 | return gulp.src('app/*.html') 55 | .pipe($.useref({searchPath: ['.tmp', 'app', '.']})) 56 | .pipe($.sourcemaps.init()) 57 | .pipe($.if('*.js', $.uglify())) 58 | //TODO has problems with bootstrap sourcemaps 59 | //.pipe($.if('*.css', $.cleanCss({compatibility: '*'}))) 60 | .pipe($.sourcemaps.write()) 61 | .pipe($.if('*.html', $.htmlmin({removeComments: true, collapseWhitespace: true}))) 62 | .pipe(gulp.dest('dist')); 63 | }); 64 | 65 | gulp.task('chromeManifest', () => { 66 | return gulp.src('app/manifest.json') 67 | .pipe($.chromeManifest({ 68 | buildnumber: true, 69 | background: { 70 | target: 'scripts/background.js', 71 | exclude: [ 72 | 'scripts/chromereload.js' 73 | ] 74 | } 75 | })) 76 | .pipe($.if('*.css', $.cleanCss({compatibility: '*'}))) 77 | .pipe($.if('*.js', $.sourcemaps.init())) 78 | .pipe($.if('*.js', $.uglify())) 79 | .pipe($.if('*.js', $.sourcemaps.write('.'))) 80 | .pipe(gulp.dest('dist')); 81 | }); 82 | 83 | gulp.task('babel', () => { 84 | return gulp.src('app/scripts.babel/**/*.js') 85 | .pipe($.babel({ 86 | presets: ['es2015'] 87 | })) 88 | .pipe(gulp.dest('app/scripts')); 89 | }); 90 | 91 | gulp.task('clean', del.bind(null, ['.tmp', 'dist'])); 92 | 93 | gulp.task('watch', ['lint', 'babel'], () => { 94 | $.livereload.listen(); 95 | 96 | gulp.watch([ 97 | 'app/*.html', 98 | 'app/scripts/**/*.js', 99 | 'app/images/**/*', 100 | 'app/styles/**/*', 101 | 'app/_locales/**/*.json' 102 | ]).on('change', $.livereload.reload); 103 | 104 | gulp.watch('app/scripts.babel/**/*.js', ['lint', 'babel']); 105 | gulp.watch('bower.json', ['wiredep']); 106 | }); 107 | 108 | gulp.task('size', () => { 109 | return gulp.src('dist/**/*').pipe($.size({title: 'build', gzip: true})); 110 | }); 111 | 112 | gulp.task('wiredep', () => { 113 | gulp.src('app/*.html') 114 | .pipe(wiredep({ 115 | ignorePath: /^(\.\.\/)*\.\./ 116 | })) 117 | .pipe(gulp.dest('app')); 118 | }); 119 | 120 | gulp.task('package', function () { 121 | var manifest = require('./dist/manifest.json'); 122 | return gulp.src('dist/**') 123 | .pipe($.zip('chrome-multiwindow-positioner-' + manifest.version + '.zip')) 124 | .pipe(gulp.dest('package')); 125 | }); 126 | 127 | gulp.task('build', (cb) => { 128 | runSequence( 129 | 'lint', 'babel', 'chromeManifest', 130 | ['html', 'images', 'extras'], 131 | 'size', cb); 132 | }); 133 | 134 | gulp.task('default', ['clean'], cb => { 135 | runSequence('build', cb); 136 | }); 137 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chrome-multiwindow-positioner", 3 | "private": true, 4 | "authors": [ 5 | "Control Expert" 6 | ], 7 | "engines": { 8 | "node": ">=0.8.0" 9 | }, 10 | "devDependencies": { 11 | "babel-core": "^6.26.3", 12 | "babel-preset-es2015": "^6.24.1", 13 | "brace-expansion": "1.1.7", 14 | "del": "^2.2.0", 15 | "gulp": "^3.9.1", 16 | "gulp-babel": "^6.1.2", 17 | "gulp-cache": "^0.4.3", 18 | "gulp-chrome-manifest": "0.0.13", 19 | "gulp-clean-css": "^2.0.3", 20 | "gulp-eslint": "^2.0.0", 21 | "gulp-htmlmin": "^1.3.0", 22 | "gulp-if": "^2.0.0", 23 | "gulp-imagemin": "^4.1.0", 24 | "gulp-livereload": "^3.8.1", 25 | "gulp-load-plugins": "^1.5.0", 26 | "gulp-size": "^2.1.0", 27 | "gulp-sourcemaps": "^1.6.0", 28 | "gulp-uglify": "^1.5.3", 29 | "gulp-useref": "^3.0.8", 30 | "gulp-zip": "^3.2.0", 31 | "main-bower-files": "^2.11.1", 32 | "run-sequence": "^1.1.5", 33 | "wiredep": "^4.0.0" 34 | }, 35 | "eslintConfig": { 36 | "env": { 37 | "node": true, 38 | "browser": true 39 | }, 40 | "globals": { 41 | "chrome": true 42 | }, 43 | "rules": { 44 | "eol-last": 0, 45 | "quotes": [ 46 | 2, 47 | "single" 48 | ] 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Mocha Spec Runner 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 18 | 19 | 20 | 21 | 22 | 23 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /test/spec/test.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | describe('Give it some context', function () { 5 | describe('maybe a bit more context here', function () { 6 | it('should run here few assertions', function () { 7 | 8 | }); 9 | }); 10 | }); 11 | })(); 12 | --------------------------------------------------------------------------------