├── .gitignore ├── .gitmodules ├── CODE_OF_CONDUCT.md ├── LICENSE.md ├── README.md ├── ResponsiveHelper.qml ├── doc ├── custom-presets-buttons.png └── demo.gif ├── examples ├── common-features-example │ ├── common-features-example.pro │ ├── main.cpp │ ├── main.qml │ └── qml.qrc ├── examples.pro └── minimal-example │ ├── main.cpp │ ├── main.qml │ ├── minimal-example.pro │ └── qml.qrc ├── responsive-helper.pro └── src ├── Button.qml ├── ResponsiveHelper-base.qml ├── TextField.qml ├── generate-qml.sh └── src.pro /.gitignore: -------------------------------------------------------------------------------- 1 | build-* 2 | *.pro.user 3 | *.temp.qml 4 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "src/combine-qml"] 2 | path = src/combine-qml 3 | url = https://github.com/Pixep/qml-files-combiner.git 4 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at Pixep@users.noreply.github.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Adrien Leravat 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 | # Qt Quick Responsive Helper 2 | A simple helper window for QtQuick based applications, to let developers test different resolutions and dpi settings easily. It was made to be integrated with minimal effort ([only one QML file](ResponsiveHelper.qml)), and to be configurable for your specific usage. 3 | 4 | ## Main features 5 | - Manually set application width and height 6 | - Manually set dpi / pixelDensity (independent from Screen.pixelDensity) 7 | - Toggle between landscape and portrait mode 8 | - Use presets to quickly test your commonly used settings 9 | - Add buttons to manage custom actions, or even custom content to the bar 10 | - Can be disabled for production with a single property 11 | 12 | Compatible with Qt 5.2 and higher, requires only QtQuick 2 and QtQuick Window module (for now). 13 | 14 | ![Responsive helper demo](doc/demo.gif) 15 | 16 | ## Installation 17 | 18 | You can either: 19 | 20 | - Copy [ResponsiveHelper.qml](ResponsiveHelper.qml) into your project 21 | - or clone the repository (`--recursive`) and use it as you see fit. The `examples` folder contains: 22 | - [minimal-example](examples/minimal-example/main.qml) 23 | - [common-features-example](examples/common-features-example/main.qml) 24 | 25 | ## Minimal working example 26 | Just drop it in your project, and set the `targetWindow` property to be the Window instance of your application: 27 | 28 | `main.qml` 29 | ``` 30 | Window { 31 | id: window 32 | 33 | ResponsiveHelper { 34 | targetWindow: window 35 | } 36 | } 37 | ``` 38 | 39 | ## Additional features 40 | 41 | ![Responsive helper window screenshot](doc/custom-presets-buttons.png) 42 | 43 | ### Presets 44 | 45 | By default, presets contains the following devices: 46 | 47 | * Galaxy Note 9 48 | * Galaxy S7 49 | * Galaxy S5 50 | * iPhone 6/7 51 | * Galaxy S3 52 | 53 | Preset will modify automatically your `window` resolution. See [DPI](#dpi) to integrate DPI settings in your application. 54 | 55 | You can define a set of custom resolutions/dpi shortcuts using the `presets` property. 56 | 57 | `main.qml` 58 | ``` 59 | Window { 60 | id: window 61 | 62 | ResponsiveHelper { 63 | id: helper 64 | targetWindow: window 65 | rootItem: rootItem 66 | 67 | // List the presets to be used for your application 68 | initialPreset: 0 69 | presets: ListModel { 70 | ListElement { label: "Galaxy S5"; width: 750; height: 1334; dpi: 326} 71 | ListElement { label: "Desktop"; width: 1280; height: 1024; dpi: 72 } 72 | } 73 | } 74 | 75 | Item { 76 | id: rootItem 77 | // Let the helper manage the size and scale of the root Item, based on available space 78 | anchors.centerIn: parent 79 | width: parent.width 80 | height: parent.height 81 | 82 | // 83 | // Use `helper.dpi`, `helper.pixelDensity`, `rootItem.width`, or `rootItem.height` 84 | } 85 | } 86 | ``` 87 | 88 | ### DPI 89 | 90 | As Qt `Screen.pixelDensity` property cannot be altered, the ResponsiveHelper provides two properties you can use instead: 91 | 92 | * The `dpi` property 93 | * in Dots per inch (actually Pixel per inch here) 94 | * The `pixelDensity` property 95 | * a drop-in replacement for Qt's `Screen.pixelDensity`, in Pixel per millimeter 96 | 97 | `main.qml` 98 | ``` 99 | Window { 100 | id: window 101 | 102 | ResponsiveHelper { 103 | id: helper 104 | targetWindow: window 105 | presets: ... 106 | } 107 | 108 | MyHeader { 109 | width: parent.width 110 | // 100 pixels high at 72 DPI 111 | height: 100 * helper.dpi / 72 112 | } 113 | } 114 | ``` 115 | 116 | ### Scaling to fit screen 117 | 118 | You can now use the `rootItem` property to define the root element that will be scaled in order to fit the content on the screen. This makes it possible to test high resolutions on a regular monitor. 119 | 120 | See the [minimal-example](examples/minimal-example/main.qml) for a working example. 121 | 122 | ### Additional action buttons 123 | 124 | Add additional action buttons with the `actions` property. 125 | 126 | ``` 127 | // Your custom action buttons 128 | actions: ListModel { 129 | ListElement { text: "MyAction1" } 130 | ListElement { text: "MyAction2" } 131 | } 132 | 133 | // Handle clicks on your actions 134 | onActionClicked: { 135 | console.log("Action " + actionIndex + " clicked") 136 | } 137 | ``` 138 | 139 | ### Custom content 140 | 141 | Custom content can also be added to the bar using the `extraContent` property. 142 | 143 | ``` 144 | // Your buttons or content 145 | extraContent: [ 146 | Button { 147 | text: "My Quit Button" 148 | width: parent.width 149 | onClicked: { 150 | window.close() 151 | } 152 | } 153 | ] 154 | ``` 155 | 156 | ## Example 157 | 158 | The example below show presets, how to add custom actions (buttons) and even arbitrary content to the bar. 159 | 160 | `main.qml` 161 | ``` 162 | // If you placed it in a folder, relative to your main.qml 163 | import "qt-quick-responsive-helper" 164 | 165 | Window { 166 | id: window 167 | width: 480 168 | height: 800 169 | 170 | ResponsiveHelper { 171 | targetWindow: window 172 | rootItem: root 173 | 174 | anchors.left: parent.right 175 | anchors.leftMargin: 30 176 | 177 | // List your common presets to be applied to your application 178 | initialPreset: 0 179 | presets: ListModel { 180 | ListElement { width: 720; height: 1024; dpi: 150 } 181 | ListElement { width: 480; height: 800 } 182 | } 183 | 184 | // Handle dpi or pixelDensity changes as you wish, instead of "Screen.pixelDensity" 185 | onDpiChanged: { } 186 | onPixelDensityChanged: { } 187 | 188 | // Add action buttons 189 | actions: ListModel { 190 | ListElement { text: "MyAction1" } 191 | ListElement { text: "MyAction2" } 192 | } 193 | // Handle clicks on your actions 194 | onActionClicked: { 195 | console.log("Action " + actionIndex + " clicked") 196 | } 197 | 198 | // ... Or add your own content directly 199 | extraContent: [ 200 | Button { 201 | text: "My custom content" 202 | width: parent.width 203 | onClicked: { 204 | window.close() 205 | } 206 | } 207 | ] 208 | } 209 | 210 | Item { 211 | id: root 212 | anchors.centerIn: parent 213 | width: parent.width 214 | height: parent.height 215 | 216 | // Your app goes here! 217 | } 218 | } 219 | ``` 220 | 221 | See the [full QML example](examples/common-features-example/main.qml) for more details. 222 | 223 | For additional details, you can have a look at the examples provided with the project, from the [Installation](#installation) chapter. 224 | 225 | ## Contribution ## 226 | Project is open to contribution, just contact me or directly hack it, if you are willing to help. 227 | -------------------------------------------------------------------------------- /ResponsiveHelper.qml: -------------------------------------------------------------------------------- 1 | //------------------------------------------------- 2 | // This file is available under the MIT license. 3 | // For more information, refer to "https://github.com/Pixep/qt-quick-responsive-helper" 4 | // Copyright 2017-2019, Adrien Leravat 5 | //------------------------------------------------- 6 | 7 | import QtQuick 2.2 8 | import QtQuick.Window 2.0 9 | 10 | Item { 11 | id: root 12 | width: defaultBarWidth 13 | height: 0 14 | 15 | //********************** 16 | // Public input properties 17 | // 18 | // Load or unloads the helper window 19 | property bool active: true 20 | 21 | // Window element of the target application to test 22 | property Window targetWindow 23 | property Item rootItem 24 | 25 | // Shows or hide responsive toolbar 26 | property bool showResponiveToolbar: true 27 | 28 | // List of presets to display 29 | property ListModel presets: ListModel { 30 | ListElement { label: "Galaxy Note 9"; width: 1440; height: 2960; dpi: 516} 31 | ListElement { label: "Galaxy S7"; width: 1440; height: 2560; dpi: 577} 32 | ListElement { label: "Galaxy S5"; width: 1080; height: 1920; dpi: 432} 33 | ListElement { label: "iPhone 6/7"; width: 750; height: 1334; dpi: 326} 34 | ListElement { label: "Galaxy S3"; width: 720; height: 1280; dpi: 306} 35 | } 36 | // Index of the initial preset used 37 | property int initialPreset: -1 38 | // Current preset index 39 | property int currentPreset: -1 40 | 41 | // Portrait or Landscape orientation 42 | readonly property int portraitMode: 0 43 | readonly property int landscapeMode: 1 44 | readonly property int orientation: 45 | (d.currentHeight > d.currentWidth) 46 | ? portraitMode 47 | : landscapeMode 48 | 49 | // List of custom actions 50 | property ListModel actions: ListModel {} 51 | 52 | // List of custom actions 53 | property alias extraContent: extraContentColumn.children 54 | 55 | //********************** 56 | // Public properties 57 | // 58 | // Custom pixel density value 59 | property real pixelDensity: Screen.pixelDensity 60 | // Custom DPI value 61 | readonly property int dpi: pixelDensity * d.pixelDensityToPpiRatio 62 | 63 | // Initial application window settings 64 | readonly property int initialWidth: d.initialWidth 65 | readonly property int initialHeight: d.initialHeight 66 | readonly property int initialPixelDensity: d.initialPixelDensity 67 | 68 | // Current width/height 69 | readonly property int currentWidth: d.currentWidth 70 | readonly property int currentHeight: d.currentHeight 71 | 72 | // Bar width 73 | readonly property int defaultBarWidth: 125 74 | 75 | //********************** 76 | // Signals 77 | // 78 | signal actionClicked(int actionIndex) 79 | 80 | //********************** 81 | // Public functions 82 | // 83 | function setDpi(dpiValue) { 84 | pixelDensity = dpiValue / d.pixelDensityToPpiRatio; 85 | } 86 | 87 | function setWindowWidth(value) { 88 | var width = (1*value).toFixed(0); 89 | d.applyWindowSize(width, d.currentHeight) 90 | } 91 | 92 | function setWindowHeight(value) { 93 | var height = (1*value).toFixed(0); 94 | d.applyWindowSize(d.currentWidth, height) 95 | } 96 | 97 | //********************** 98 | // Internal logic 99 | // 100 | onTargetWindowChanged: { 101 | if (initialPreset >= 0) { 102 | d.setPreset(initialPreset); 103 | } 104 | 105 | d.initialWidth = targetWindow.width; 106 | d.currentWidth = targetWindow.width; 107 | d.initialHeight = targetWindow.height; 108 | d.currentHeight = targetWindow.height; 109 | d.initialPixelDensity = root.pixelDensity; 110 | } 111 | 112 | onDpiChanged: { 113 | var preset = presets.get(root.currentPreset); 114 | if (preset && root.dpi !== preset.dpi) 115 | root.currentPreset = -1 116 | } 117 | 118 | onCurrentPresetChanged: { 119 | d.setPreset(currentPreset); 120 | } 121 | 122 | QtObject { 123 | id: d 124 | readonly property real pixelDensityToPpiRatio: 25.4 125 | property int initialWidth 126 | property int initialHeight 127 | property real initialPixelDensity: Screen.pixelDensity 128 | property real initialDpi: initialPixelDensity * pixelDensityToPpiRatio 129 | 130 | property int currentWidth 131 | property int currentHeight 132 | 133 | property int lastPresetSelected: root.initialPreset 134 | 135 | property real widthMaxScale: 1 136 | property real heightMaxScale: 1 137 | 138 | property int textHeight: 20 139 | readonly property real sizeIncrementFactor: 1.1; 140 | 141 | function updateCurrentPreset() { 142 | var preset = presets.get(root.currentPreset); 143 | if (!preset || (targetWindow.width !== preset.width || targetWindow.height !== preset.height)) { 144 | for (var i = 0; i < presets.count; ++i) { 145 | var p = presets.get(i) 146 | if (p.width === targetWindow.width && p.height === targetWindow.height) { 147 | root.currentPreset = i 148 | return 149 | } 150 | } 151 | root.currentPreset = -1 152 | } 153 | } 154 | 155 | function setPreset(index) { 156 | if (index < 0 || index > presets.count-1) { 157 | return; 158 | } 159 | 160 | if (root.currentPreset !== index) { 161 | root.currentPreset = index 162 | return; 163 | } 164 | 165 | applyWindowSize(presets.get(index).width, presets.get(index).height); 166 | 167 | if (presets.get(index).dpi) 168 | setDpi(presets.get(index).dpi) 169 | else 170 | setDpi(d.initialDpi) 171 | } 172 | 173 | function applyWindowSize(width, height) { 174 | var previousWindowWidth = targetWindow.width; 175 | var previousWindowX = targetWindow.x; 176 | 177 | if (root.rootItem) { 178 | var maxSizeFactor = 0.85; 179 | if (width > maxSizeFactor * Screen.width) { 180 | d.widthMaxScale = (maxSizeFactor * Screen.width / width); 181 | } else { 182 | d.widthMaxScale = 1; 183 | } 184 | 185 | if (height > maxSizeFactor * Screen.height) { 186 | d.heightMaxScale = (maxSizeFactor * Screen.height / height); 187 | } else { 188 | d.heightMaxScale = 1; 189 | } 190 | 191 | var scale = Math.min(d.widthMaxScale, d.heightMaxScale); 192 | var actualWidth = scale * width; 193 | var actualHeight = scale * height; 194 | 195 | if (targetWindow.x + actualWidth > Screen.width) { 196 | targetWindow.x = (Screen.width - actualWidth) / 2; 197 | } 198 | if (targetWindow.y + actualHeight > Screen.height) { 199 | targetWindow.y = (Screen.height - actualHeight) / 2; 200 | } 201 | 202 | targetWindow.width = actualWidth; 203 | targetWindow.height = actualHeight; 204 | root.rootItem.scale = scale; 205 | root.rootItem.width = width; 206 | root.rootItem.height = height; 207 | } else { 208 | targetWindow.width = width; 209 | targetWindow.height = height; 210 | } 211 | 212 | var widthDelta = targetWindow.width - previousWindowWidth; 213 | 214 | // Move the application window to keep our window at the same spot when possible 215 | if (root.x < targetWindow.x / 2) { 216 | var availableSpace = Screen.width - previousWindowX - previousWindowWidth; 217 | if (widthDelta > 0 && availableSpace <= widthDelta) 218 | targetWindow.x -= widthDelta - availableSpace; 219 | } 220 | else { 221 | if (widthDelta < 0) 222 | targetWindow.x -= widthDelta; 223 | else if (previousWindowX > 0) 224 | targetWindow.x = Math.max(0, previousWindowX - widthDelta); 225 | } 226 | 227 | d.currentWidth = width; 228 | d.currentHeight = height; 229 | } 230 | } 231 | 232 | Connections { 233 | target: targetWindow 234 | onWidthChanged: { d.updateCurrentPreset() } 235 | onHeightChanged: { d.updateCurrentPreset() } 236 | } 237 | 238 | Loader { 239 | active: root.active && root.targetWindow 240 | sourceComponent: responsiveHelperComponent 241 | } 242 | 243 | Column { 244 | id: extraContentColumn 245 | width: parent.width 246 | visible: false 247 | } 248 | 249 | //********************** 250 | // GUI 251 | // 252 | Component { 253 | id: responsiveHelperComponent 254 | 255 | Window { 256 | id: helperWindow 257 | visible: true 258 | x: targetWindow.x + root.x + windowOffset.x 259 | y: targetWindow.y + root.y + windowOffset.y 260 | width: root.width 261 | height: root.height 262 | color: "#202020" 263 | flags: Qt.FramelessWindowHint 264 | contentItem.opacity: handleMouseArea.pressed ? 0.3 : 1 265 | 266 | property point windowOffset: Qt.point(0, 0) 267 | 268 | Component.onCompleted: { 269 | root.width = Qt.binding(function() { return barColumn.width; }); 270 | root.height = Qt.binding(function() { return barColumn.height; }); 271 | } 272 | 273 | Connections { 274 | target: targetWindow 275 | onClosing: { 276 | helperWindow.close(); 277 | } 278 | onActiveChanged: { 279 | helperWindow.raise(); 280 | } 281 | } 282 | 283 | Connections { 284 | target: root 285 | onTargetWindowChanged: { 286 | dpiEdit.bind(); 287 | widthEdit.bind(); 288 | heightEdit.bind(); 289 | } 290 | } 291 | 292 | Column { 293 | id: barColumn 294 | spacing: 1 295 | width: root.width 296 | 297 | Component.onCompleted: { 298 | extraContentColumn.parent = barColumn 299 | extraContentColumn.visible = true 300 | } 301 | 302 | MouseArea { 303 | id: handleMouseArea 304 | width: parent.width 305 | height: 20 306 | 307 | property point originMousePosition 308 | 309 | onPressed: { 310 | originMousePosition.x = mouseX 311 | originMousePosition.y = mouseY 312 | } 313 | onReleased: { 314 | helperWindow.windowOffset.x += mouseX - originMousePosition.x 315 | helperWindow.windowOffset.y += mouseY - originMousePosition.y 316 | } 317 | 318 | Grid { 319 | anchors.centerIn: parent 320 | columns: 5 321 | rows: 2 322 | spacing: 3 323 | Repeater { model: 10; Rectangle { width: 4; height: width; radius: width/2 } } 324 | } 325 | } 326 | 327 | //--------------- 328 | // @Button 329 | Rectangle { 330 | color: baseColor 331 | height: 30 332 | 333 | property bool selected: false 334 | property color baseColor: "#555" 335 | 336 | signal clicked 337 | 338 | Rectangle { 339 | anchors.fill: parent 340 | color: "#FFF" 341 | opacity: 0.3 342 | visible: parent.selected 343 | } 344 | 345 | Text { 346 | text: parent.text 347 | anchors.centerIn: parent 348 | color: parent.selected ? "#FFF" : "#EEE" 349 | } 350 | 351 | MouseArea { 352 | anchors.fill: parent 353 | onClicked: { 354 | parent.clicked() 355 | } 356 | onPressed: { 357 | parent.color = Qt.lighter(parent.baseColor) 358 | } 359 | onReleased: { 360 | parent.color = parent.baseColor 361 | } 362 | } 363 | //---- Redefinitions ---- 364 | property string text: "Hide" 365 | width: parent.width 366 | onClicked: { 367 | helperWindow.close() 368 | } 369 | } 370 | 371 | Item { 372 | width: parent.width 373 | height: 10 374 | } 375 | 376 | //*************************************************************************** 377 | // Responsive-related settings 378 | // 379 | Column { 380 | width: parent.width 381 | height: visible ? childrenRect.height : 0 382 | visible: root.showResponiveToolbar 383 | spacing: 1 384 | 385 | //--------------- 386 | // @Button 387 | Rectangle { 388 | color: baseColor 389 | height: 30 390 | 391 | property bool selected: false 392 | property color baseColor: "#555" 393 | 394 | signal clicked 395 | 396 | Rectangle { 397 | anchors.fill: parent 398 | color: "#FFF" 399 | opacity: 0.3 400 | visible: parent.selected 401 | } 402 | 403 | Text { 404 | text: parent.text 405 | anchors.centerIn: parent 406 | color: parent.selected ? "#FFF" : "#EEE" 407 | } 408 | 409 | MouseArea { 410 | anchors.fill: parent 411 | onClicked: { 412 | parent.clicked() 413 | } 414 | onPressed: { 415 | parent.color = Qt.lighter(parent.baseColor) 416 | } 417 | onReleased: { 418 | parent.color = parent.baseColor 419 | } 420 | } 421 | //---- Redefinitions ---- 422 | width: parent.width 423 | property string text: (root.orientation === root.portraitMode) ? "Portrait" 424 | : "Landscape" 425 | onClicked: { 426 | d.applyWindowSize(d.currentHeight, d.currentWidth); 427 | } 428 | } 429 | 430 | //--------------- 431 | // @Button 432 | Rectangle { 433 | color: baseColor 434 | height: 30 435 | 436 | property bool selected: false 437 | property color baseColor: "#555" 438 | 439 | signal clicked 440 | 441 | Rectangle { 442 | anchors.fill: parent 443 | color: "#FFF" 444 | opacity: 0.3 445 | visible: parent.selected 446 | } 447 | 448 | Text { 449 | text: parent.text 450 | anchors.centerIn: parent 451 | color: parent.selected ? "#FFF" : "#EEE" 452 | } 453 | 454 | MouseArea { 455 | anchors.fill: parent 456 | onClicked: { 457 | parent.clicked() 458 | } 459 | onPressed: { 460 | parent.color = Qt.lighter(parent.baseColor) 461 | } 462 | onReleased: { 463 | parent.color = parent.baseColor 464 | } 465 | } 466 | //---- Redefinitions ---- 467 | property string text: "Reset" 468 | width: parent.width 469 | onClicked: { 470 | d.applyWindowSize(d.initialWidth, d.initialHeight); 471 | root.pixelDensity = d.initialPixelDensity; 472 | d.lastPresetSelected = -1; 473 | } 474 | } 475 | 476 | //********************** 477 | // DPI 478 | // 479 | Text { 480 | text: "DPI" 481 | color: "white" 482 | height: d.textHeight 483 | width: parent.width 484 | wrapMode: Text.Wrap 485 | horizontalAlignment: Text.AlignHCenter 486 | verticalAlignment: Text.AlignBottom 487 | } 488 | 489 | Row { 490 | width: parent.width 491 | height: childrenRect.height 492 | spacing: 1 493 | 494 | //--------------- 495 | // @Button 496 | Rectangle { 497 | color: baseColor 498 | 499 | property bool selected: false 500 | property color baseColor: "#555" 501 | 502 | signal clicked 503 | 504 | Rectangle { 505 | anchors.fill: parent 506 | color: "#FFF" 507 | opacity: 0.3 508 | visible: parent.selected 509 | } 510 | 511 | Text { 512 | text: parent.text 513 | anchors.centerIn: parent 514 | color: parent.selected ? "#FFF" : "#EEE" 515 | } 516 | 517 | MouseArea { 518 | anchors.fill: parent 519 | onClicked: { 520 | parent.clicked() 521 | } 522 | onPressed: { 523 | parent.color = Qt.lighter(parent.baseColor) 524 | } 525 | onReleased: { 526 | parent.color = parent.baseColor 527 | } 528 | } 529 | //---- Redefinitions ---- 530 | height: dpiEdit.height 531 | width: parent.width / 4 532 | property string text: "-" 533 | onClicked: { 534 | root.pixelDensity /= 1.3 535 | } 536 | } 537 | //--------------- 538 | // @TextField 539 | Rectangle { 540 | color: "#555" 541 | height: 30 542 | 543 | property string text 544 | 545 | signal discarded() 546 | signal editingFinished(string value) 547 | 548 | TextInput { 549 | anchors.fill: parent 550 | horizontalAlignment: TextEdit.AlignHCenter 551 | verticalAlignment: TextEdit.AlignVCenter 552 | color: "#EEE" 553 | font.bold: true 554 | validator: IntValidator{bottom: 0; top: 5000;} 555 | property Item componentRoot: parent 556 | 557 | onFocusChanged: { 558 | parent.color = focus ? "#999" : "#555" 559 | } 560 | Component.onCompleted: { 561 | bind() 562 | validator.bottom = parent.minimum 563 | validator.top = parent.maximum 564 | } 565 | Keys.onEscapePressed: { 566 | focus = false 567 | bind() 568 | } 569 | onEditingFinished: { 570 | focus = false 571 | parent.editingFinished(text) 572 | bind() 573 | } 574 | function bind() { 575 | text = Qt.binding(function() { return parent.text } ) 576 | } 577 | } 578 | //---- Redefinitions ---- 579 | id: dpiEdit 580 | width: parent.width / 2 581 | text: root.dpi.toFixed(0) 582 | property int minimum: 1 583 | property int maximum: 999 584 | onEditingFinished: { 585 | root.setDpi(value) 586 | } 587 | } 588 | 589 | //--------------- 590 | // @Button 591 | Rectangle { 592 | color: baseColor 593 | 594 | property bool selected: false 595 | property color baseColor: "#555" 596 | 597 | signal clicked 598 | 599 | Rectangle { 600 | anchors.fill: parent 601 | color: "#FFF" 602 | opacity: 0.3 603 | visible: parent.selected 604 | } 605 | 606 | Text { 607 | text: parent.text 608 | anchors.centerIn: parent 609 | color: parent.selected ? "#FFF" : "#EEE" 610 | } 611 | 612 | MouseArea { 613 | anchors.fill: parent 614 | onClicked: { 615 | parent.clicked() 616 | } 617 | onPressed: { 618 | parent.color = Qt.lighter(parent.baseColor) 619 | } 620 | onReleased: { 621 | parent.color = parent.baseColor 622 | } 623 | } 624 | //---- Redefinitions ---- 625 | height: dpiEdit.height 626 | width: parent.width / 4 627 | property string text: "+" 628 | onClicked: { 629 | root.pixelDensity *= 1.3 630 | } 631 | } 632 | } 633 | 634 | //********************** 635 | // Width 636 | // 637 | Text { 638 | text: "Width" 639 | color: "white" 640 | height: d.textHeight 641 | width: parent.width 642 | wrapMode: Text.Wrap 643 | horizontalAlignment: Text.AlignHCenter 644 | verticalAlignment: Text.AlignBottom 645 | } 646 | 647 | Row { 648 | id: row 649 | width: parent.width 650 | height: childrenRect.height 651 | spacing: 1 652 | 653 | //--------------- 654 | // @Button 655 | Rectangle { 656 | color: baseColor 657 | 658 | property bool selected: false 659 | property color baseColor: "#555" 660 | 661 | signal clicked 662 | 663 | Rectangle { 664 | anchors.fill: parent 665 | color: "#FFF" 666 | opacity: 0.3 667 | visible: parent.selected 668 | } 669 | 670 | Text { 671 | text: parent.text 672 | anchors.centerIn: parent 673 | color: parent.selected ? "#FFF" : "#EEE" 674 | } 675 | 676 | MouseArea { 677 | anchors.fill: parent 678 | onClicked: { 679 | parent.clicked() 680 | } 681 | onPressed: { 682 | parent.color = Qt.lighter(parent.baseColor) 683 | } 684 | onReleased: { 685 | parent.color = parent.baseColor 686 | } 687 | } 688 | //---- Redefinitions ---- 689 | height: widthEdit.height 690 | width: parent.width / 4 691 | property string text: "-" 692 | onClicked: { 693 | root.setWindowWidth(d.currentWidth / d.sizeIncrementFactor) 694 | } 695 | } 696 | //--------------- 697 | // @TextField 698 | Rectangle { 699 | color: "#555" 700 | height: 30 701 | 702 | property string text 703 | 704 | signal discarded() 705 | signal editingFinished(string value) 706 | 707 | TextInput { 708 | anchors.fill: parent 709 | horizontalAlignment: TextEdit.AlignHCenter 710 | verticalAlignment: TextEdit.AlignVCenter 711 | color: "#EEE" 712 | font.bold: true 713 | validator: IntValidator{bottom: 0; top: 5000;} 714 | property Item componentRoot: parent 715 | 716 | onFocusChanged: { 717 | parent.color = focus ? "#999" : "#555" 718 | } 719 | Component.onCompleted: { 720 | bind() 721 | validator.bottom = parent.minimum 722 | validator.top = parent.maximum 723 | } 724 | Keys.onEscapePressed: { 725 | focus = false 726 | bind() 727 | } 728 | onEditingFinished: { 729 | focus = false 730 | parent.editingFinished(text) 731 | bind() 732 | } 733 | function bind() { 734 | text = Qt.binding(function() { return parent.text } ) 735 | } 736 | } 737 | //---- Redefinitions ---- 738 | id: widthEdit 739 | width: parent.width / 2 740 | property int minimum: 10 741 | property int maximum: 5000 742 | text: d.currentWidth 743 | 744 | onEditingFinished: { 745 | root.setWindowWidth(value) 746 | } 747 | } 748 | 749 | //--------------- 750 | // @Button 751 | Rectangle { 752 | color: baseColor 753 | 754 | property bool selected: false 755 | property color baseColor: "#555" 756 | 757 | signal clicked 758 | 759 | Rectangle { 760 | anchors.fill: parent 761 | color: "#FFF" 762 | opacity: 0.3 763 | visible: parent.selected 764 | } 765 | 766 | Text { 767 | text: parent.text 768 | anchors.centerIn: parent 769 | color: parent.selected ? "#FFF" : "#EEE" 770 | } 771 | 772 | MouseArea { 773 | anchors.fill: parent 774 | onClicked: { 775 | parent.clicked() 776 | } 777 | onPressed: { 778 | parent.color = Qt.lighter(parent.baseColor) 779 | } 780 | onReleased: { 781 | parent.color = parent.baseColor 782 | } 783 | } 784 | //---- Redefinitions ---- 785 | height: widthEdit.height 786 | width: parent.width / 4 787 | property string text: "+" 788 | onClicked: { 789 | root.setWindowWidth(d.currentWidth * d.sizeIncrementFactor) 790 | } 791 | } 792 | } 793 | 794 | //********************** 795 | // Height 796 | // 797 | Text { 798 | text: "Height" 799 | color: "white" 800 | width: parent.width 801 | height: d.textHeight 802 | wrapMode: Text.Wrap 803 | horizontalAlignment: Text.AlignHCenter 804 | verticalAlignment: Text.AlignBottom 805 | } 806 | 807 | Row { 808 | width: parent.width 809 | height: childrenRect.height 810 | spacing: 1 811 | 812 | //--------------- 813 | // @Button 814 | Rectangle { 815 | color: baseColor 816 | 817 | property bool selected: false 818 | property color baseColor: "#555" 819 | 820 | signal clicked 821 | 822 | Rectangle { 823 | anchors.fill: parent 824 | color: "#FFF" 825 | opacity: 0.3 826 | visible: parent.selected 827 | } 828 | 829 | Text { 830 | text: parent.text 831 | anchors.centerIn: parent 832 | color: parent.selected ? "#FFF" : "#EEE" 833 | } 834 | 835 | MouseArea { 836 | anchors.fill: parent 837 | onClicked: { 838 | parent.clicked() 839 | } 840 | onPressed: { 841 | parent.color = Qt.lighter(parent.baseColor) 842 | } 843 | onReleased: { 844 | parent.color = parent.baseColor 845 | } 846 | } 847 | //---- Redefinitions ---- 848 | height: heightEdit.height 849 | width: parent.width / 4 850 | property string text: "-" 851 | onClicked: { 852 | root.setWindowHeight(d.currentHeight / d.sizeIncrementFactor) 853 | } 854 | } 855 | //--------------- 856 | // @TextField 857 | Rectangle { 858 | color: "#555" 859 | height: 30 860 | 861 | property string text 862 | 863 | signal discarded() 864 | signal editingFinished(string value) 865 | 866 | TextInput { 867 | anchors.fill: parent 868 | horizontalAlignment: TextEdit.AlignHCenter 869 | verticalAlignment: TextEdit.AlignVCenter 870 | color: "#EEE" 871 | font.bold: true 872 | validator: IntValidator{bottom: 0; top: 5000;} 873 | property Item componentRoot: parent 874 | 875 | onFocusChanged: { 876 | parent.color = focus ? "#999" : "#555" 877 | } 878 | Component.onCompleted: { 879 | bind() 880 | validator.bottom = parent.minimum 881 | validator.top = parent.maximum 882 | } 883 | Keys.onEscapePressed: { 884 | focus = false 885 | bind() 886 | } 887 | onEditingFinished: { 888 | focus = false 889 | parent.editingFinished(text) 890 | bind() 891 | } 892 | function bind() { 893 | text = Qt.binding(function() { return parent.text } ) 894 | } 895 | } 896 | //---- Redefinitions ---- 897 | id: heightEdit 898 | width: parent.width / 2 899 | text: d.currentHeight 900 | property int minimum: 10 901 | property int maximum: 5000 902 | 903 | onEditingFinished: { 904 | root.setWindowHeight(value) 905 | } 906 | } 907 | 908 | //--------------- 909 | // @Button 910 | Rectangle { 911 | color: baseColor 912 | 913 | property bool selected: false 914 | property color baseColor: "#555" 915 | 916 | signal clicked 917 | 918 | Rectangle { 919 | anchors.fill: parent 920 | color: "#FFF" 921 | opacity: 0.3 922 | visible: parent.selected 923 | } 924 | 925 | Text { 926 | text: parent.text 927 | anchors.centerIn: parent 928 | color: parent.selected ? "#FFF" : "#EEE" 929 | } 930 | 931 | MouseArea { 932 | anchors.fill: parent 933 | onClicked: { 934 | parent.clicked() 935 | } 936 | onPressed: { 937 | parent.color = Qt.lighter(parent.baseColor) 938 | } 939 | onReleased: { 940 | parent.color = parent.baseColor 941 | } 942 | } 943 | //---- Redefinitions ---- 944 | height: heightEdit.height 945 | width: parent.width / 4 946 | property string text: "+" 947 | onClicked: { 948 | root.setWindowHeight(d.currentHeight * d.sizeIncrementFactor) 949 | } 950 | } 951 | } 952 | 953 | //********************** 954 | // Presets 955 | // 956 | Text { 957 | text: "Presets" 958 | width: parent.width 959 | height: d.textHeight 960 | color: "white" 961 | wrapMode: Text.Wrap 962 | horizontalAlignment: Text.AlignHCenter 963 | verticalAlignment: Text.AlignBottom 964 | visible: root.presets.count > 0 965 | } 966 | 967 | Repeater { 968 | model: root.presets 969 | 970 | //--------------- 971 | // @Button 972 | Rectangle { 973 | color: baseColor 974 | height: 30 975 | 976 | property color baseColor: "#555" 977 | 978 | signal clicked 979 | 980 | Rectangle { 981 | anchors.fill: parent 982 | color: "#FFF" 983 | opacity: 0.3 984 | visible: parent.selected 985 | } 986 | 987 | Text { 988 | text: parent.text 989 | anchors.centerIn: parent 990 | color: parent.selected ? "#FFF" : "#EEE" 991 | } 992 | 993 | MouseArea { 994 | anchors.fill: parent 995 | onClicked: { 996 | parent.clicked() 997 | } 998 | onPressed: { 999 | parent.color = Qt.lighter(parent.baseColor) 1000 | } 1001 | onReleased: { 1002 | parent.color = parent.baseColor 1003 | } 1004 | } 1005 | //---- Redefinitions ---- 1006 | width: parent.width 1007 | property string text: { 1008 | var label = ""; 1009 | if (model.label) { 1010 | label = model.label; 1011 | } else { 1012 | label = model.width + " x " + model.height; 1013 | 1014 | if (model.dpi) 1015 | label += " (" + model.dpi + "dpi)"; 1016 | } 1017 | 1018 | return label; 1019 | } 1020 | property bool selected: d.lastPresetSelected === index 1021 | onClicked: { 1022 | root.currentPreset = index; 1023 | d.lastPresetSelected = index; 1024 | } 1025 | } 1026 | } 1027 | } 1028 | 1029 | //********************** 1030 | // Actions & Buttons 1031 | // 1032 | Text { 1033 | text: "Actions" 1034 | width: parent.width 1035 | height: d.textHeight 1036 | color: "white" 1037 | wrapMode: Text.Wrap 1038 | horizontalAlignment: Text.AlignHCenter 1039 | verticalAlignment: Text.AlignBottom 1040 | visible: root.actions.count > 0 1041 | } 1042 | 1043 | Repeater { 1044 | model: root.actions 1045 | 1046 | //--------------- 1047 | // @Button 1048 | Rectangle { 1049 | color: baseColor 1050 | height: 30 1051 | 1052 | property bool selected: false 1053 | property color baseColor: "#555" 1054 | 1055 | signal clicked 1056 | 1057 | Rectangle { 1058 | anchors.fill: parent 1059 | color: "#FFF" 1060 | opacity: 0.3 1061 | visible: parent.selected 1062 | } 1063 | 1064 | Text { 1065 | text: parent.text 1066 | anchors.centerIn: parent 1067 | color: parent.selected ? "#FFF" : "#EEE" 1068 | } 1069 | 1070 | MouseArea { 1071 | anchors.fill: parent 1072 | onClicked: { 1073 | parent.clicked() 1074 | } 1075 | onPressed: { 1076 | parent.color = Qt.lighter(parent.baseColor) 1077 | } 1078 | onReleased: { 1079 | parent.color = parent.baseColor 1080 | } 1081 | } 1082 | //---- Redefinitions ---- 1083 | width: parent.width 1084 | property string text: model.text 1085 | onClicked: { 1086 | root.actionClicked(index); 1087 | } 1088 | } 1089 | } 1090 | } 1091 | } 1092 | } 1093 | } 1094 | -------------------------------------------------------------------------------- /doc/custom-presets-buttons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pixep/qt-quick-responsive-helper/362d0a0bb94dfd8b815fb4ccdea5b0ede3c15c6c/doc/custom-presets-buttons.png -------------------------------------------------------------------------------- /doc/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pixep/qt-quick-responsive-helper/362d0a0bb94dfd8b815fb4ccdea5b0ede3c15c6c/doc/demo.gif -------------------------------------------------------------------------------- /examples/common-features-example/common-features-example.pro: -------------------------------------------------------------------------------- 1 | TEMPLATE = app 2 | 3 | QT += qml quick 4 | CONFIG += c++11 5 | 6 | SOURCES += main.cpp 7 | RESOURCES += qml.qrc 8 | 9 | OTHER_FILES += \ 10 | ../../ResponsiveHelper.qml \ 11 | main.qml 12 | 13 | # Default rules for deployment. 14 | qnx: target.path = /tmp/$${TARGET}/bin 15 | else: unix:!android: target.path = /opt/$${TARGET}/bin 16 | !isEmpty(target.path): INSTALLS += target 17 | -------------------------------------------------------------------------------- /examples/common-features-example/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | int main(int argc, char *argv[]) 5 | { 6 | QGuiApplication app(argc, argv); 7 | 8 | QQmlApplicationEngine engine; 9 | engine.load(QUrl(QStringLiteral("qrc:/main.qml"))); 10 | 11 | return app.exec(); 12 | } 13 | -------------------------------------------------------------------------------- /examples/common-features-example/main.qml: -------------------------------------------------------------------------------- 1 | //------------------------------------------------- 2 | // This file is available under the MIT license. 3 | // For more information, refer to "https://github.com/Pixep/qt-quick-responsive-helper" 4 | // Copyright 2017-2019, Adrien Leravat 5 | //------------------------------------------------- 6 | 7 | import QtQuick 2.2 8 | import QtQuick.Window 2.0 9 | import QtQuick.Controls 1.0 10 | 11 | Window { 12 | id: window 13 | visible: true 14 | width: 640 15 | height: 480 16 | title: qsTr("Responsive helper example") 17 | 18 | // This demonstrates some settings of the component 19 | ResponsiveHelper { 20 | id: helperBar 21 | width: 125 22 | 23 | // Reference to your Window 24 | targetWindow: window 25 | 26 | // Reference to the content root, for resizing and scaling 27 | // It is not possible to use window.contentItem. If not set, 28 | // the content will not be scaled to fit on the screen. 29 | rootItem: root 30 | 31 | // Can be completely disabled (not loaded) in production environment 32 | active: true 33 | 34 | // Hide responsive-related buttons (dpi, resolution) 35 | //showResponiveToolbar: false 36 | 37 | // Position it where you want 38 | anchors.left: parent.right 39 | anchors.leftMargin: 30 40 | 41 | // Lists the presets (resolution, dpi) shown as shortcuts for your application 42 | initialPreset: 0 43 | presets: ListModel { 44 | ListElement { width: 720; height: 1024; dpi: 150} 45 | ListElement { label: "My 800x480 preset"; width: 480; height: 800; dpi: 72 } 46 | } 47 | 48 | // Your custom action buttons 49 | actions: ListModel { 50 | ListElement { text: "MyAction1" } 51 | ListElement { text: "MyAction2" } 52 | } 53 | 54 | // Handle clicks on your actions 55 | onActionClicked: { 56 | console.log("Action " + actionIndex + " clicked") 57 | } 58 | 59 | // Your buttons or content 60 | extraContent: [ 61 | Button { 62 | text: "My custom content" 63 | width: parent.width 64 | onClicked: { 65 | window.close() 66 | } 67 | } 68 | ] 69 | 70 | // Handle dpi or pixelDensity changes as you wish, instead of "Screen.pixelDensity" 71 | onDpiChanged: { 72 | console.log("Dpi set to " + dpi) 73 | } 74 | onPixelDensityChanged: { 75 | //console.log("Pixel density set to " + pixelDensity) 76 | } 77 | } 78 | 79 | // Root item used to scale the content to fit on the screen. 80 | Item { 81 | id: root 82 | anchors.centerIn: parent 83 | 84 | // width, height and scale will be adapted automatically 85 | width: parent.width 86 | height: parent.height 87 | 88 | //------------------------------------------------------------ 89 | // Simple usage example 90 | //------------------------------------------------------------ 91 | property real scaleFactor: width / 640 92 | property real dpiScaleFactor: helperBar.dpi / 20 93 | 94 | Rectangle { 95 | id: header 96 | width: parent.width 97 | height: 30 * root.scaleFactor 98 | color: "#DDD" 99 | 100 | Text { 101 | id: textEdit 102 | text: qsTr("Some text") 103 | font.pixelSize: 20 * root.scaleFactor 104 | anchors.centerIn: parent 105 | } 106 | } 107 | 108 | Flickable { 109 | width: parent.width 110 | anchors.top: header.bottom 111 | anchors.topMargin: 10 112 | height: parent.height - header.height - header.y 113 | contentHeight: flow.height 114 | clip: true 115 | 116 | Grid { 117 | id: flow 118 | height: childrenRect.height 119 | width: columns * (spacing + rectSize) 120 | anchors.horizontalCenter: parent.horizontalCenter 121 | spacing: 10 122 | columns: parent.width / (spacing + rectSize) 123 | rows: 15 124 | 125 | property int rectSize: 25 * root.dpiScaleFactor 126 | 127 | Repeater { 128 | model: 15 129 | Rectangle { 130 | width: parent.rectSize 131 | height: width 132 | color: "blue" 133 | } 134 | } 135 | } 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /examples/common-features-example/qml.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | main.qml 4 | ../../ResponsiveHelper.qml 5 | 6 | 7 | -------------------------------------------------------------------------------- /examples/examples.pro: -------------------------------------------------------------------------------- 1 | TEMPLATE = subdirs 2 | 3 | SUBDIRS += \ 4 | minimal-example \ 5 | common-features-example 6 | -------------------------------------------------------------------------------- /examples/minimal-example/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | int main(int argc, char *argv[]) 5 | { 6 | QGuiApplication app(argc, argv); 7 | 8 | QQmlApplicationEngine engine; 9 | engine.load(QUrl(QStringLiteral("qrc:/main.qml"))); 10 | 11 | return app.exec(); 12 | } 13 | -------------------------------------------------------------------------------- /examples/minimal-example/main.qml: -------------------------------------------------------------------------------- 1 | //------------------------------------------------- 2 | // This file is available under the MIT license. 3 | // For more information, refer to "https://github.com/Pixep/qt-quick-responsive-helper" 4 | // Copyright 2017-2019, Adrien Leravat 5 | //------------------------------------------------- 6 | 7 | import QtQuick 2.2 8 | import QtQuick.Window 2.0 9 | 10 | Window { 11 | id: window 12 | visible: true 13 | width: 640 14 | height: 480 15 | title: qsTr("Responsive helper example") 16 | 17 | // Minimal example, simply include this in your QML 18 | ResponsiveHelper { 19 | id: helperBar 20 | 21 | // Reference to your Window 22 | targetWindow: window 23 | 24 | // Reference to the content root, for resizing and scaling 25 | // It is not possible to use window.contentItem. If not set, 26 | // the content will not be scaled to fit on the screen. 27 | rootItem: root 28 | 29 | // Position it where you want 30 | anchors.left: parent.right 31 | anchors.leftMargin: 30 32 | } 33 | 34 | // Root item used to scale the content to fit on the screen. 35 | Item { 36 | id: root 37 | anchors.centerIn: parent 38 | 39 | // width, height and scale will be adapted automatically 40 | width: parent.width 41 | height: parent.height 42 | 43 | // Scale with resolution, from a 640x480 reference 44 | property real resolutionScaleFactor: (width * height) / (640 * 480) 45 | // Scale with pixel density, from a 72ppi reference 46 | property real dpiScaleFactor: helperBar.dpi / 72 47 | 48 | Rectangle { 49 | id: header 50 | width: parent.width 51 | height: childrenRect.height 52 | color: "#DDD" 53 | 54 | Column { 55 | Text { 56 | text: qsTr("Scales with resolution") 57 | font.pixelSize: 20 * root.resolutionScaleFactor 58 | } 59 | Text { 60 | text: qsTr("Scales with DPI") 61 | font.pixelSize: 20 * root.dpiScaleFactor 62 | } 63 | } 64 | } 65 | 66 | Flickable { 67 | width: parent.width 68 | anchors.top: header.bottom 69 | anchors.topMargin: 10 70 | height: parent.height - header.height - header.y 71 | contentHeight: flow.height 72 | clip: true 73 | 74 | Grid { 75 | id: flow 76 | height: childrenRect.height 77 | width: columns * (spacing + rectSize) 78 | anchors.horizontalCenter: parent.horizontalCenter 79 | spacing: 10 80 | columns: parent.width / (spacing + rectSize) 81 | rows: 15 82 | 83 | property int rectSize: 25 * root.dpiScaleFactor 84 | 85 | Repeater { 86 | model: 15 87 | Rectangle { 88 | width: parent.rectSize 89 | height: width 90 | color: "blue" 91 | } 92 | } 93 | } 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /examples/minimal-example/minimal-example.pro: -------------------------------------------------------------------------------- 1 | TEMPLATE = app 2 | 3 | QT += qml quick 4 | CONFIG += c++11 5 | 6 | SOURCES += main.cpp 7 | RESOURCES += qml.qrc 8 | 9 | OTHER_FILES += \ 10 | ../../ResponsiveHelper.qml \ 11 | main.qml 12 | 13 | # Default rules for deployment. 14 | qnx: target.path = /tmp/$${TARGET}/bin 15 | else: unix:!android: target.path = /opt/$${TARGET}/bin 16 | !isEmpty(target.path): INSTALLS += target 17 | -------------------------------------------------------------------------------- /examples/minimal-example/qml.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | main.qml 4 | ../../ResponsiveHelper.qml 5 | 6 | 7 | -------------------------------------------------------------------------------- /responsive-helper.pro: -------------------------------------------------------------------------------- 1 | TEMPLATE = subdirs 2 | 3 | SUBDIRS = \ 4 | examples \ 5 | src 6 | 7 | OTHER_FILES = \ 8 | ResponsiveHelper.qml 9 | -------------------------------------------------------------------------------- /src/Button.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.2 2 | Rectangle { 3 | color: baseColor 4 | width: 100 5 | height: 30 6 | 7 | property string text: "" 8 | property bool selected: false 9 | property color baseColor: "#555" 10 | 11 | signal clicked 12 | 13 | Rectangle { 14 | anchors.fill: parent 15 | color: "#FFF" 16 | opacity: 0.3 17 | visible: parent.selected 18 | } 19 | 20 | Text { 21 | text: parent.text 22 | anchors.centerIn: parent 23 | color: parent.selected ? "#FFF" : "#EEE" 24 | } 25 | 26 | MouseArea { 27 | anchors.fill: parent 28 | onClicked: { 29 | parent.clicked() 30 | } 31 | onPressed: { 32 | parent.color = Qt.lighter(parent.baseColor) 33 | } 34 | onReleased: { 35 | parent.color = parent.baseColor 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/ResponsiveHelper-base.qml: -------------------------------------------------------------------------------- 1 | //------------------------------------------------- 2 | // This file is available under the MIT license. 3 | // For more information, refer to "https://github.com/Pixep/qt-quick-responsive-helper" 4 | // Copyright 2017-2019, Adrien Leravat 5 | //------------------------------------------------- 6 | 7 | import QtQuick 2.2 8 | import QtQuick.Window 2.0 9 | 10 | Item { 11 | id: root 12 | width: defaultBarWidth 13 | height: 0 14 | 15 | //********************** 16 | // Public input properties 17 | // 18 | // Load or unloads the helper window 19 | property bool active: true 20 | 21 | // Window element of the target application to test 22 | property Window targetWindow 23 | property Item rootItem 24 | 25 | // Shows or hide responsive toolbar 26 | property bool showResponiveToolbar: true 27 | 28 | // List of presets to display 29 | property ListModel presets: ListModel { 30 | ListElement { label: "Galaxy Note 9"; width: 1440; height: 2960; dpi: 516} 31 | ListElement { label: "Galaxy S7"; width: 1440; height: 2560; dpi: 577} 32 | ListElement { label: "Galaxy S5"; width: 1080; height: 1920; dpi: 432} 33 | ListElement { label: "iPhone 6/7"; width: 750; height: 1334; dpi: 326} 34 | ListElement { label: "Galaxy S3"; width: 720; height: 1280; dpi: 306} 35 | } 36 | // Index of the initial preset used 37 | property int initialPreset: -1 38 | // Current preset index 39 | property int currentPreset: -1 40 | 41 | // Portrait or Landscape orientation 42 | readonly property int portraitMode: 0 43 | readonly property int landscapeMode: 1 44 | readonly property int orientation: 45 | (d.currentHeight > d.currentWidth) 46 | ? portraitMode 47 | : landscapeMode 48 | 49 | // List of custom actions 50 | property ListModel actions: ListModel {} 51 | 52 | // List of custom actions 53 | property alias extraContent: extraContentColumn.children 54 | 55 | //********************** 56 | // Public properties 57 | // 58 | // Custom pixel density value 59 | property real pixelDensity: Screen.pixelDensity 60 | // Custom DPI value 61 | readonly property int dpi: pixelDensity * d.pixelDensityToPpiRatio 62 | 63 | // Initial application window settings 64 | readonly property int initialWidth: d.initialWidth 65 | readonly property int initialHeight: d.initialHeight 66 | readonly property int initialPixelDensity: d.initialPixelDensity 67 | 68 | // Current width/height 69 | readonly property int currentWidth: d.currentWidth 70 | readonly property int currentHeight: d.currentHeight 71 | 72 | // Bar width 73 | readonly property int defaultBarWidth: 125 74 | 75 | //********************** 76 | // Signals 77 | // 78 | signal actionClicked(int actionIndex) 79 | 80 | //********************** 81 | // Public functions 82 | // 83 | function setDpi(dpiValue) { 84 | pixelDensity = dpiValue / d.pixelDensityToPpiRatio; 85 | } 86 | 87 | function setWindowWidth(value) { 88 | var width = (1*value).toFixed(0); 89 | d.applyWindowSize(width, d.currentHeight) 90 | } 91 | 92 | function setWindowHeight(value) { 93 | var height = (1*value).toFixed(0); 94 | d.applyWindowSize(d.currentWidth, height) 95 | } 96 | 97 | //********************** 98 | // Internal logic 99 | // 100 | onTargetWindowChanged: { 101 | if (initialPreset >= 0) { 102 | d.setPreset(initialPreset); 103 | } 104 | 105 | d.initialWidth = targetWindow.width; 106 | d.currentWidth = targetWindow.width; 107 | d.initialHeight = targetWindow.height; 108 | d.currentHeight = targetWindow.height; 109 | d.initialPixelDensity = root.pixelDensity; 110 | } 111 | 112 | onDpiChanged: { 113 | var preset = presets.get(root.currentPreset); 114 | if (preset && root.dpi !== preset.dpi) 115 | root.currentPreset = -1 116 | } 117 | 118 | onCurrentPresetChanged: { 119 | d.setPreset(currentPreset); 120 | } 121 | 122 | QtObject { 123 | id: d 124 | readonly property real pixelDensityToPpiRatio: 25.4 125 | property int initialWidth 126 | property int initialHeight 127 | property real initialPixelDensity: Screen.pixelDensity 128 | property real initialDpi: initialPixelDensity * pixelDensityToPpiRatio 129 | 130 | property int currentWidth 131 | property int currentHeight 132 | 133 | property int lastPresetSelected: root.initialPreset 134 | 135 | property real widthMaxScale: 1 136 | property real heightMaxScale: 1 137 | 138 | property int textHeight: 20 139 | readonly property real sizeIncrementFactor: 1.1; 140 | 141 | function updateCurrentPreset() { 142 | var preset = presets.get(root.currentPreset); 143 | if (!preset || (targetWindow.width !== preset.width || targetWindow.height !== preset.height)) { 144 | for (var i = 0; i < presets.count; ++i) { 145 | var p = presets.get(i) 146 | if (p.width === targetWindow.width && p.height === targetWindow.height) { 147 | root.currentPreset = i 148 | return 149 | } 150 | } 151 | root.currentPreset = -1 152 | } 153 | } 154 | 155 | function setPreset(index) { 156 | if (index < 0 || index > presets.count-1) { 157 | return; 158 | } 159 | 160 | if (root.currentPreset !== index) { 161 | root.currentPreset = index 162 | return; 163 | } 164 | 165 | applyWindowSize(presets.get(index).width, presets.get(index).height); 166 | 167 | if (presets.get(index).dpi) 168 | setDpi(presets.get(index).dpi) 169 | else 170 | setDpi(d.initialDpi) 171 | } 172 | 173 | function applyWindowSize(width, height) { 174 | var previousWindowWidth = targetWindow.width; 175 | var previousWindowX = targetWindow.x; 176 | 177 | if (root.rootItem) { 178 | var maxSizeFactor = 0.85; 179 | if (width > maxSizeFactor * Screen.width) { 180 | d.widthMaxScale = (maxSizeFactor * Screen.width / width); 181 | } else { 182 | d.widthMaxScale = 1; 183 | } 184 | 185 | if (height > maxSizeFactor * Screen.height) { 186 | d.heightMaxScale = (maxSizeFactor * Screen.height / height); 187 | } else { 188 | d.heightMaxScale = 1; 189 | } 190 | 191 | var scale = Math.min(d.widthMaxScale, d.heightMaxScale); 192 | var actualWidth = scale * width; 193 | var actualHeight = scale * height; 194 | 195 | if (targetWindow.x + actualWidth > Screen.width) { 196 | targetWindow.x = (Screen.width - actualWidth) / 2; 197 | } 198 | if (targetWindow.y + actualHeight > Screen.height) { 199 | targetWindow.y = (Screen.height - actualHeight) / 2; 200 | } 201 | 202 | targetWindow.width = actualWidth; 203 | targetWindow.height = actualHeight; 204 | root.rootItem.scale = scale; 205 | root.rootItem.width = width; 206 | root.rootItem.height = height; 207 | } else { 208 | targetWindow.width = width; 209 | targetWindow.height = height; 210 | } 211 | 212 | var widthDelta = targetWindow.width - previousWindowWidth; 213 | 214 | // Move the application window to keep our window at the same spot when possible 215 | if (root.x < targetWindow.x / 2) { 216 | var availableSpace = Screen.width - previousWindowX - previousWindowWidth; 217 | if (widthDelta > 0 && availableSpace <= widthDelta) 218 | targetWindow.x -= widthDelta - availableSpace; 219 | } 220 | else { 221 | if (widthDelta < 0) 222 | targetWindow.x -= widthDelta; 223 | else if (previousWindowX > 0) 224 | targetWindow.x = Math.max(0, previousWindowX - widthDelta); 225 | } 226 | 227 | d.currentWidth = width; 228 | d.currentHeight = height; 229 | } 230 | } 231 | 232 | Connections { 233 | target: targetWindow 234 | onWidthChanged: { d.updateCurrentPreset() } 235 | onHeightChanged: { d.updateCurrentPreset() } 236 | } 237 | 238 | Loader { 239 | active: root.active && root.targetWindow 240 | sourceComponent: responsiveHelperComponent 241 | } 242 | 243 | Column { 244 | id: extraContentColumn 245 | width: parent.width 246 | visible: false 247 | } 248 | 249 | //********************** 250 | // GUI 251 | // 252 | Component { 253 | id: responsiveHelperComponent 254 | 255 | Window { 256 | id: helperWindow 257 | visible: true 258 | x: targetWindow.x + root.x + windowOffset.x 259 | y: targetWindow.y + root.y + windowOffset.y 260 | width: root.width 261 | height: root.height 262 | color: "#202020" 263 | flags: Qt.FramelessWindowHint 264 | contentItem.opacity: handleMouseArea.pressed ? 0.3 : 1 265 | 266 | property point windowOffset: Qt.point(0, 0) 267 | 268 | Component.onCompleted: { 269 | root.width = Qt.binding(function() { return barColumn.width; }); 270 | root.height = Qt.binding(function() { return barColumn.height; }); 271 | } 272 | 273 | Connections { 274 | target: targetWindow 275 | onClosing: { 276 | helperWindow.close(); 277 | } 278 | onActiveChanged: { 279 | helperWindow.raise(); 280 | } 281 | } 282 | 283 | Connections { 284 | target: root 285 | onTargetWindowChanged: { 286 | dpiEdit.bind(); 287 | widthEdit.bind(); 288 | heightEdit.bind(); 289 | } 290 | } 291 | 292 | Column { 293 | id: barColumn 294 | spacing: 1 295 | width: root.width 296 | 297 | Component.onCompleted: { 298 | extraContentColumn.parent = barColumn 299 | extraContentColumn.visible = true 300 | } 301 | 302 | MouseArea { 303 | id: handleMouseArea 304 | width: parent.width 305 | height: 20 306 | 307 | property point originMousePosition 308 | 309 | onPressed: { 310 | originMousePosition.x = mouseX 311 | originMousePosition.y = mouseY 312 | } 313 | onReleased: { 314 | helperWindow.windowOffset.x += mouseX - originMousePosition.x 315 | helperWindow.windowOffset.y += mouseY - originMousePosition.y 316 | } 317 | 318 | Grid { 319 | anchors.centerIn: parent 320 | columns: 5 321 | rows: 2 322 | spacing: 3 323 | Repeater { model: 10; Rectangle { width: 4; height: width; radius: width/2 } } 324 | } 325 | } 326 | 327 | @Button { 328 | text: "Hide" 329 | width: parent.width 330 | onClicked: { 331 | helperWindow.close() 332 | } 333 | } 334 | 335 | Item { 336 | width: parent.width 337 | height: 10 338 | } 339 | 340 | //*************************************************************************** 341 | // Responsive-related settings 342 | // 343 | Column { 344 | width: parent.width 345 | height: visible ? childrenRect.height : 0 346 | visible: root.showResponiveToolbar 347 | spacing: 1 348 | 349 | @Button { 350 | width: parent.width 351 | text: (root.orientation === root.portraitMode) ? "Portrait" 352 | : "Landscape" 353 | onClicked: { 354 | d.applyWindowSize(d.currentHeight, d.currentWidth); 355 | } 356 | } 357 | 358 | @Button { 359 | text: "Reset" 360 | width: parent.width 361 | onClicked: { 362 | d.applyWindowSize(d.initialWidth, d.initialHeight); 363 | root.pixelDensity = d.initialPixelDensity; 364 | d.lastPresetSelected = -1; 365 | } 366 | } 367 | 368 | //********************** 369 | // DPI 370 | // 371 | Text { 372 | text: "DPI" 373 | color: "white" 374 | height: d.textHeight 375 | width: parent.width 376 | wrapMode: Text.Wrap 377 | horizontalAlignment: Text.AlignHCenter 378 | verticalAlignment: Text.AlignBottom 379 | } 380 | 381 | Row { 382 | width: parent.width 383 | height: childrenRect.height 384 | spacing: 1 385 | 386 | @Button { 387 | height: dpiEdit.height 388 | width: parent.width / 4 389 | text: "-" 390 | onClicked: { 391 | root.pixelDensity /= 1.3 392 | } 393 | } 394 | @TextField { 395 | id: dpiEdit 396 | width: parent.width / 2 397 | text: root.dpi.toFixed(0) 398 | minimum: 1 399 | maximum: 999 400 | onEditingFinished: { 401 | root.setDpi(value) 402 | } 403 | } 404 | 405 | @Button { 406 | height: dpiEdit.height 407 | width: parent.width / 4 408 | text: "+" 409 | onClicked: { 410 | root.pixelDensity *= 1.3 411 | } 412 | } 413 | } 414 | 415 | //********************** 416 | // Width 417 | // 418 | Text { 419 | text: "Width" 420 | color: "white" 421 | height: d.textHeight 422 | width: parent.width 423 | wrapMode: Text.Wrap 424 | horizontalAlignment: Text.AlignHCenter 425 | verticalAlignment: Text.AlignBottom 426 | } 427 | 428 | Row { 429 | id: row 430 | width: parent.width 431 | height: childrenRect.height 432 | spacing: 1 433 | 434 | @Button { 435 | height: widthEdit.height 436 | width: parent.width / 4 437 | text: "-" 438 | onClicked: { 439 | root.setWindowWidth(d.currentWidth / d.sizeIncrementFactor) 440 | } 441 | } 442 | @TextField { 443 | id: widthEdit 444 | width: parent.width / 2 445 | minimum: 10 446 | maximum: 5000 447 | text: d.currentWidth 448 | 449 | onEditingFinished: { 450 | root.setWindowWidth(value) 451 | } 452 | } 453 | 454 | @Button { 455 | height: widthEdit.height 456 | width: parent.width / 4 457 | text: "+" 458 | onClicked: { 459 | root.setWindowWidth(d.currentWidth * d.sizeIncrementFactor) 460 | } 461 | } 462 | } 463 | 464 | //********************** 465 | // Height 466 | // 467 | Text { 468 | text: "Height" 469 | color: "white" 470 | width: parent.width 471 | height: d.textHeight 472 | wrapMode: Text.Wrap 473 | horizontalAlignment: Text.AlignHCenter 474 | verticalAlignment: Text.AlignBottom 475 | } 476 | 477 | Row { 478 | width: parent.width 479 | height: childrenRect.height 480 | spacing: 1 481 | 482 | @Button { 483 | height: heightEdit.height 484 | width: parent.width / 4 485 | text: "-" 486 | onClicked: { 487 | root.setWindowHeight(d.currentHeight / d.sizeIncrementFactor) 488 | } 489 | } 490 | @TextField { 491 | id: heightEdit 492 | width: parent.width / 2 493 | text: d.currentHeight 494 | minimum: 10 495 | maximum: 5000 496 | 497 | onEditingFinished: { 498 | root.setWindowHeight(value) 499 | } 500 | } 501 | 502 | @Button { 503 | height: heightEdit.height 504 | width: parent.width / 4 505 | text: "+" 506 | onClicked: { 507 | root.setWindowHeight(d.currentHeight * d.sizeIncrementFactor) 508 | } 509 | } 510 | } 511 | 512 | //********************** 513 | // Presets 514 | // 515 | Text { 516 | text: "Presets" 517 | width: parent.width 518 | height: d.textHeight 519 | color: "white" 520 | wrapMode: Text.Wrap 521 | horizontalAlignment: Text.AlignHCenter 522 | verticalAlignment: Text.AlignBottom 523 | visible: root.presets.count > 0 524 | } 525 | 526 | Repeater { 527 | model: root.presets 528 | 529 | @Button { 530 | width: parent.width 531 | text: { 532 | var label = ""; 533 | if (model.label) { 534 | label = model.label; 535 | } else { 536 | label = model.width + " x " + model.height; 537 | 538 | if (model.dpi) 539 | label += " (" + model.dpi + "dpi)"; 540 | } 541 | 542 | return label; 543 | } 544 | selected: d.lastPresetSelected === index 545 | onClicked: { 546 | root.currentPreset = index; 547 | d.lastPresetSelected = index; 548 | } 549 | } 550 | } 551 | } 552 | 553 | //********************** 554 | // Actions & Buttons 555 | // 556 | Text { 557 | text: "Actions" 558 | width: parent.width 559 | height: d.textHeight 560 | color: "white" 561 | wrapMode: Text.Wrap 562 | horizontalAlignment: Text.AlignHCenter 563 | verticalAlignment: Text.AlignBottom 564 | visible: root.actions.count > 0 565 | } 566 | 567 | Repeater { 568 | model: root.actions 569 | 570 | @Button { 571 | width: parent.width 572 | text: model.text 573 | onClicked: { 574 | root.actionClicked(index); 575 | } 576 | } 577 | } 578 | } 579 | } 580 | } 581 | } 582 | -------------------------------------------------------------------------------- /src/TextField.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.2 2 | Rectangle { 3 | color: "#555" 4 | width: 100 5 | height: 30 6 | 7 | property string text 8 | property int minimum: 0 9 | property int maximum: 5000 10 | 11 | signal discarded() 12 | signal editingFinished(string value) 13 | 14 | TextInput { 15 | anchors.fill: parent 16 | horizontalAlignment: TextEdit.AlignHCenter 17 | verticalAlignment: TextEdit.AlignVCenter 18 | color: "#EEE" 19 | font.bold: true 20 | validator: IntValidator{bottom: 0; top: 5000;} 21 | property Item componentRoot: parent 22 | 23 | onFocusChanged: { 24 | parent.color = focus ? "#999" : "#555" 25 | } 26 | Component.onCompleted: { 27 | bind() 28 | validator.bottom = parent.minimum 29 | validator.top = parent.maximum 30 | } 31 | Keys.onEscapePressed: { 32 | focus = false 33 | bind() 34 | } 35 | onEditingFinished: { 36 | focus = false 37 | parent.editingFinished(text) 38 | bind() 39 | } 40 | function bind() { 41 | text = Qt.binding(function() { return parent.text } ) 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/generate-qml.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | DIR="$( cd "$( dirname "$0" )" && pwd )" 4 | python2 ${DIR}/combine-qml/combine-qml.py ${DIR}/ResponsiveHelper-base.qml ${DIR}/../ResponsiveHelper.qml -c ${DIR}/Button.qml Button -c ${DIR}/TextField.qml TextField 5 | -------------------------------------------------------------------------------- /src/src.pro: -------------------------------------------------------------------------------- 1 | TEMPLATE = aux 2 | 3 | DISTFILES += \ 4 | Button.qml \ 5 | TextField.qml \ 6 | ResponsiveHelper-base.qml 7 | --------------------------------------------------------------------------------