diff --git a/README.md b/README.md index 91807eec8..4e852c718 100644 --- a/README.md +++ b/README.md @@ -38,10 +38,11 @@ Using one of the methods below, install the qmlweb JavaScript library: * [Bower](http://bower.io/search/?q=qmlweb) — `bower install qmlweb` * GitHub [releases](https://github.com/qmlweb/qmlweb/releases) — `tar -xaf v0.2.0.tar.gz` -* Manually (recommended if you cloned from git) — `npm install && npm run build` +* Manually (recommended if you cloned from git) — +`npm install && npm run build` -Next, simply add `lib/qt.js` to the list of other JavaScript files in your app's -HTML file: +Next, simply add `lib/qt.js` to the list of other JavaScript files in +your app's HTML file: ```HTML diff --git a/docs/QMLEngine.md b/docs/QMLEngine.md index 9ab6cbee9..0a2518003 100644 --- a/docs/QMLEngine.md +++ b/docs/QMLEngine.md @@ -66,8 +66,8 @@ base path extracted from the initially loaded file path. ### Implicit input -* engine object function `importPathList()` - list of urls bases used for qmldir - files lookup +* engine object function `importPathList()` - list of urls bases used for + qmldir files lookup ### Additional implicit input/output @@ -115,8 +115,8 @@ This hash then used by `qml.js::construct` method for computing component urls. #### TODO * We have to keep output in component scope, not in engine scope. -* We have to add module "as"-names to component's names (which is possible after - keeping imports in component scope). +* We have to add module "as"-names to component's names (which is possible + after keeping imports in component scope). * Determine how this stuff is related to `QmlWeb.loadImports` * Check A1 * Make a complete picture of what going in with imports, including Component.js diff --git a/docs/Signal.md b/docs/Signal.md index 99cc935df..24f338300 100644 --- a/docs/Signal.md +++ b/docs/Signal.md @@ -29,5 +29,5 @@ methods from the `signal`. ## Class Method: Signal.signal(\[params\[, options\]\]) -Constructs a `Signal` instance with the given `params` and `options` and returns -its `.signal` property. +Constructs a `Signal` instance with the given `params` and `options` and +returns its `.signal` property. diff --git a/karma.conf.js b/karma.conf.js index 1ff84d7b8..c4512d120 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -11,6 +11,7 @@ module.exports = function(config) { "tests/common.js", "tests/failingTests.js", "tests/*/*.js", + "tests/*/**/test*.js", { pattern: "tests/*/**/qmldir", included: false }, { pattern: "tests/*/**/qml/*.js", included: false }, { pattern: "tests/*/**/*.qml", included: false }, @@ -22,6 +23,8 @@ module.exports = function(config) { type: "lcov", dir: "coverage/" }, + browserDisconnectTolerance: 5, // required for phantomjs in windows + browserNoActivityTimeout: 100000, // required for phantomjs in windows customLaunchers: { PhantomJSCustom: { base: "PhantomJS", diff --git a/src/engine/properties.js b/src/engine/properties.js index bbd2d7feb..84e713a21 100644 --- a/src/engine/properties.js +++ b/src/engine/properties.js @@ -253,14 +253,14 @@ function connectSignal(item, signalName, value, objectScope, componentScope) { value.src = `( function(${params.join(", ")}) { QmlWeb.executionContext = __executionContext; - QmlWeb.engine.$oldBasePath = QmlWeb.engine.$basePath; + const bp = QmlWeb.engine.$basePath; QmlWeb.engine.$basePath = "${componentScope.$basePath}"; try { (function() { ${value.src} })(); } finally { - QmlWeb.engine.$basePath = QmlWeb.engine.$oldBasePath; + QmlWeb.engine.$basePath = bp; } } )`; diff --git a/src/modules/QtQuick.Controls/ComboBox.js b/src/modules/QtQuick.Controls/ComboBox.js index f4846e891..7f43139e0 100644 --- a/src/modules/QtQuick.Controls/ComboBox.js +++ b/src/modules/QtQuick.Controls/ComboBox.js @@ -5,7 +5,7 @@ QmlWeb.registerQmlType({ baseClass: "QtQuick.Item", properties: { count: "int", - currentIndex: "int", + currentIndex: { type: "int", initialValue: 0 }, // same in QtQuick.Controls currentText: "string", menu: { type: "array", initialValue: [] }, model: { type: "array", initialValue: [] }, @@ -22,8 +22,15 @@ QmlWeb.registerQmlType({ this.dom.style.pointerEvents = "auto"; this.name = "QMLComboBox"; + // TODO change innerHTML to DOM + this.dom.innerHTML = ""; + this.impl = this.dom.firstChild; + this.Component.completed.connect(this, this.Component$onCompleted); this.modelChanged.connect(this, this.$onModelChanged); + this.currentIndexChanged.connect(this, this.$onCurrentIndexChanged); + this.heightChanged.connect(this, this.$onHeightChanged); + this.widthChanged.connect(this, this.$onWidthChanged); this.dom.onclick = () => { const index = this.dom.firstChild.selectedIndex; @@ -43,29 +50,59 @@ QmlWeb.registerQmlType({ return this.model[index]; } $updateImpl() { - this.currentIndex = 0; this.count = this.model.length; - const entries = []; - for (let i = 0; i < this.count; i++) { - const elt = this.model[i]; - //if (elt instanceof Array) { // TODO - optgroups? update model ! - // var count_i = elt.length; - // for (var j = 0; j < count_i; j++) - // html += ""; - //} - //else - entries.push(``); + + const k = this.count; const m = this.model; + + this.impl.options.length = k; + for (let i = 0; i < k; i++) { + this.impl.options[i] = new Option(m[i]); } - // TODO: remove innerHTML, port to DOM - this.dom.innerHTML = ``; - this.impl = this.dom.firstChild; + + // should call this, because width()/heights() invoke updateV(H)Geometry, + // which in turn sets valid $useImplicitHeight flag + const h = this.height; const w = this.width; + + this.implicitWidth = this.impl.offsetWidth; + this.implicitHeight = this.impl.offsetHeight; + + this.$onHeightChanged(h); + this.$onWidthChanged(w); + + this.impl.selectedIndex = this.currentIndex; + this.$updateCurrentText(); } Component$onCompleted() { this.$updateImpl(); - this.implicitWidth = this.impl.offsetWidth; - this.implicitHeight = this.impl.offsetHeight; } $onModelChanged() { this.$updateImpl(); } + $onCurrentIndexChanged() { + const i = this.currentIndex; + if (this.impl.selectedIndex !== i) { + this.impl.selectedIndex = i; + this.$updateCurrentText(); + this.activated(i); + } + } + $updateCurrentText() { + if (typeof this.currentIndex === "undefined" || !this.model) { + this.currentText = undefined; + } else if (this.currentIndex >= 0 && + this.currentIndex < this.model.length) { + this.currentText = this.model[ this.currentIndex ]; + } + } + $onHeightChanged() { + if (this.height > 0 && this.impl + && this.height !== this.impl.offsetHeight) { + this.impl.style.height = `${this.height}px`; + } + } + $onWidthChanged() { + if (this.width > 0 && this.impl && this.width !== this.impl.offsetWidth) { + this.impl.style.width = `${this.width}px`; + } + } }); diff --git a/src/modules/QtQuick.Controls/ScrollView.js b/src/modules/QtQuick.Controls/ScrollView.js index e77ab1363..a8b5aa384 100644 --- a/src/modules/QtQuick.Controls/ScrollView.js +++ b/src/modules/QtQuick.Controls/ScrollView.js @@ -45,9 +45,13 @@ QmlWeb.registerQmlType({ this.viewport = undefined; this.frameVisible = false; this.highlightOnFocus = false; + this.verticalScrollBarPolicy = Qt.ScrollBarAsNeeded; this.horizontalScrollBarPolicy = Qt.ScrollBarAsNeeded; this.style = undefined; + + this.$onVerticalScrollBarPolicyChanged(this.verticalScrollBarPolicy); + this.$onHorizontalScrollBarPolicyChanged(this.horizontalScrollBarPolicy); } $onContentItemChanged(newItem) { if (typeof newItem !== undefined) { diff --git a/src/modules/QtQuick/Column.js b/src/modules/QtQuick/Column.js index 9e12011ba..2a7f3f1dd 100644 --- a/src/modules/QtQuick/Column.js +++ b/src/modules/QtQuick/Column.js @@ -8,7 +8,7 @@ QmlWeb.registerQmlType({ QmlWeb.callSuper(this, meta); } layoutChildren() { - let curPos = 0; + let curPos = this.padding; let maxWidth = 0; for (let i = 0; i < this.children.length; i++) { const child = this.children[i]; @@ -16,11 +16,12 @@ QmlWeb.registerQmlType({ continue; } maxWidth = child.width > maxWidth ? child.width : maxWidth; - child.y = curPos; + child.y = curPos + this.padding; + if (this.padding > 0) child.x = this.padding; curPos += child.height + this.spacing; } - this.implicitWidth = maxWidth; - this.implicitHeight = curPos - this.spacing; + this.implicitWidth = maxWidth + this.padding * 2; + this.implicitHeight = curPos - this.spacing + this.padding; // We want no spacing at the bottom side } }); diff --git a/src/modules/QtQuick/Item.js b/src/modules/QtQuick/Item.js index 7688b3453..884a5e352 100644 --- a/src/modules/QtQuick/Item.js +++ b/src/modules/QtQuick/Item.js @@ -314,7 +314,7 @@ QmlWeb.registerQmlType({ $onHeightChanged_(newVal) { this.css.height = newVal ? `${newVal}px` : "auto"; } - $onFocusChanged(newVal) { + $onFocusChanged_(newVal) { if (newVal) { if (this.dom.firstChild) { this.dom.firstChild.focus(); @@ -365,6 +365,9 @@ QmlWeb.registerQmlType({ } if (typeof this.z === "number") { transform += ` translate3d(0, 0, ${this.z}px)`; + // should also consider z as zIndex for stacking order behaviour of qml + // see http://doc.qt.io/qt-5/qml-qtquick-item.html#z-prop + this.dom.style.zIndex = this.z; } this.dom.style.transform = transform; this.dom.style.transformStyle = transformStyle; diff --git a/src/modules/QtQuick/MouseArea.js b/src/modules/QtQuick/MouseArea.js index 16df1b78b..2aa0e0c05 100644 --- a/src/modules/QtQuick/MouseArea.js +++ b/src/modules/QtQuick/MouseArea.js @@ -18,7 +18,8 @@ QmlWeb.registerQmlType({ clicked: [{ type: "variant", name: "mouse" }], entered: [], exited: [], - positionChanged: [{ type: "variant", name: "mouse" }] + positionChanged: [{ type: "variant", name: "mouse" }], + wheel: [{ type: "variant", name: "wheel" }] } }, class { constructor(meta) { @@ -82,6 +83,9 @@ QmlWeb.registerQmlType({ if (!this.enabled || !this.hoverEnabled || this.pressed) return; this.$handlePositionChanged(e); }); + this.dom.addEventListener("wheel", e => { + this.$handleWheel(e); + }); } $onCursorShapeChanged() { this.dom.style.cursor = this.$cursorShapeToCSS(); @@ -92,6 +96,18 @@ QmlWeb.registerQmlType({ this.mouseY = mouse.y; this.positionChanged(mouse); } + $handleWheel(e) { + const wheel = this.$eventToMouse(e); + wheel.angleDelta = { x: e.deltaX, y: e.deltaY }; + wheel.accepted = false; + + this.wheel(wheel); + + if (wheel.accepted) { + e.stopPropagation(); + e.preventDefault(); + } + } $handleClick(e) { const mouse = this.$eventToMouse(e); if (this.enabled && this.acceptedButtons & mouse.button) { diff --git a/src/modules/QtQuick/Positioner.js b/src/modules/QtQuick/Positioner.js index 2ba1587d9..f4c407c59 100644 --- a/src/modules/QtQuick/Positioner.js +++ b/src/modules/QtQuick/Positioner.js @@ -4,7 +4,8 @@ QmlWeb.registerQmlType({ versions: /.*/, baseClass: "Item", properties: { - spacing: "int" + spacing: "int", + padding: "int" } }, class { constructor(meta) { diff --git a/src/modules/QtQuick/Row.js b/src/modules/QtQuick/Row.js index 646a2be43..a5b496c1d 100644 --- a/src/modules/QtQuick/Row.js +++ b/src/modules/QtQuick/Row.js @@ -14,7 +14,7 @@ QmlWeb.registerQmlType({ this.layoutChildren(); } layoutChildren() { - let curPos = 0; + let curPos = this.padding; let maxHeight = 0; // When layoutDirection is RightToLeft we need oposite order let i = this.layoutDirection === 1 ? this.children.length - 1 : 0; @@ -28,10 +28,12 @@ QmlWeb.registerQmlType({ maxHeight = child.height > maxHeight ? child.height : maxHeight; child.x = curPos; + if (this.padding > 0) child.y = this.padding; + curPos += child.width + this.spacing; } - this.implicitHeight = maxHeight; + this.implicitHeight = maxHeight + this.padding * 2; // We want no spacing at the right side - this.implicitWidth = curPos - this.spacing; + this.implicitWidth = curPos - this.spacing + this.padding; } }); diff --git a/src/qtbase/Signal.js b/src/qtbase/Signal.js index 082e4d72a..af1ae583f 100644 --- a/src/qtbase/Signal.js +++ b/src/qtbase/Signal.js @@ -113,11 +113,14 @@ class Signal { } static $execute(desc, args) { + // TODO: remove this try/catch in test env try { desc.slot.apply(desc.thisObj, args); } catch (err) { console.error("Signal slot error:", err.message, err, - Function.prototype.toString.call(desc.slot) + desc.slot + ? Function.prototype.toString.call(desc.slot) + : "desc.slot is undefined!" ); } } diff --git a/tests/QMLEngine/basepath/B.qml b/tests/QMLEngine/basepath/B.qml new file mode 100644 index 000000000..bfec8d60c --- /dev/null +++ b/tests/QMLEngine/basepath/B.qml @@ -0,0 +1,5 @@ +import QtQuick 2.0 +import QtQuick.Controls 1.2 + +Item { +} \ No newline at end of file diff --git a/tests/QMLEngine/basepath/import/A.qml b/tests/QMLEngine/basepath/import/A.qml new file mode 100644 index 000000000..6d7d445e0 --- /dev/null +++ b/tests/QMLEngine/basepath/import/A.qml @@ -0,0 +1,13 @@ +import QtQuick 2.0 +import QtQuick.Controls 1.2 + +Item { + property var a + property var b + + onAChanged: foo1() + onBChanged: foo2() + + function foo1() { b = a+1; } + function foo2() {} +} \ No newline at end of file diff --git a/tests/QMLEngine/basepath/test.js b/tests/QMLEngine/basepath/test.js new file mode 100644 index 000000000..8c52cf716 --- /dev/null +++ b/tests/QMLEngine/basepath/test.js @@ -0,0 +1,12 @@ +describe("QMLEngine.basepath", function() { + setupDivElement(); + var webroot = "/base/tests/QMLEngine/basepath/"; + + // this checks the case of evalling properties during component init + // when this component is imported from another directory + // followed by loading of component in current directory + it("engine.$basePath is not corrupted after recursive properties eval", + function() { + var qml = loadQmlFile(webroot + "test.qml", this.div); + }); +}); diff --git a/tests/QMLEngine/basepath/test.qml b/tests/QMLEngine/basepath/test.qml new file mode 100644 index 000000000..7bdc9bacd --- /dev/null +++ b/tests/QMLEngine/basepath/test.qml @@ -0,0 +1,11 @@ +import QtQuick 2.0 +import QtQuick.Controls 1.2 +import "import" + +Item { + A { + a:1 + } + B { + } +} \ No newline at end of file diff --git a/tests/QtQuick.Controls/ComboBox/test.js b/tests/QtQuick.Controls/ComboBox/test.js new file mode 100644 index 000000000..7ede8daae --- /dev/null +++ b/tests/QtQuick.Controls/ComboBox/test.js @@ -0,0 +1,60 @@ +describe("QtQuick.ComboBox", function() { + setupDivElement(); + var webroot = "/base/tests/QtQuick.Controls/ComboBox/"; + + it("implicitWidth should change with model", function() { + var qml = loadQmlFile(webroot + "test.qml", this.div); + + qml.model = ["hello i am a long string!"]; + expect(qml.implicitWidth).toBeGreaterThan(100); + + qml.model = ["hi!"]; + expect(qml.implicitWidth).toBeLessThan(100); + + expect(qml.width).toBe(qml.implicitWidth); + }); + + it("Can specify explicit width for ComboBox", function() { + var qml = loadQmlFile(webroot + "test.qml", this.div); + + qml.width = 400; + qml.model = ["hello i am a long string!"]; + + expect(qml.width).toBe(400); + + qml.width = 300; + expect(qml.width).toBe(300); + }); + + it("should auto-select first value", function() { + var qml = loadQmlFile(webroot + "test.qml", this.div); + + qml.model = ["a", "b", "c"]; + + expect(qml.currentText).toBe("a"); + expect(qml.currentIndex).toBe(0); + }); + + it("change of currentIndex should involve change of currentText", function() { + var qml = loadQmlFile(webroot + "test.qml", this.div); + + qml.model = ["a", "b", "c"]; + qml.currentIndex = 1; + expect(qml.currentText).toBe("b"); + }); + + it("default value of currentIndex should be 0", function() { + var qml = loadQmlFile(webroot + "test.qml", this.div); + + qml.model = ["a", "b", "c"]; + expect(qml.currentText).toBe("a"); + expect(qml.currentIndex).toBe(0); + }); + + it("initial value of currentIndex should be considered", function() { + var qml = loadQmlFile(webroot + "test_populated.qml", this.div); + + expect(qml.currentText).toBe("b"); + expect(qml.currentIndex).toBe(1); + }); +}); diff --git a/tests/QtQuick.Controls/ComboBox/test.qml b/tests/QtQuick.Controls/ComboBox/test.qml new file mode 100644 index 000000000..a847e4dbe --- /dev/null +++ b/tests/QtQuick.Controls/ComboBox/test.qml @@ -0,0 +1,5 @@ +import QtQuick 2.0 +import QtQuick.Controls 1.2 + +ComboBox { +} \ No newline at end of file diff --git a/tests/QtQuick.Controls/ComboBox/test_populated.qml b/tests/QtQuick.Controls/ComboBox/test_populated.qml new file mode 100644 index 000000000..e1e4cb3ac --- /dev/null +++ b/tests/QtQuick.Controls/ComboBox/test_populated.qml @@ -0,0 +1,7 @@ +import QtQuick 2.0 +import QtQuick.Controls 1.2 + +ComboBox { + currentIndex: 1 + model: ["a","b","c"] +} \ No newline at end of file