├── .editorconfig ├── .gitignore ├── .manifest ├── LICENSE ├── core ├── Activity.qml ├── ActivityManager.qml ├── AnimatedSprite.qml ├── Audio.qml ├── BaseActivity.qml ├── DebugInfoItem.qml ├── DebugInfoPanel.qml ├── FragmentActivity.qml ├── Highlighter.qml ├── JsonStorage.qml ├── LazyActivity.qml ├── LazyPage.qml ├── LazyPageStack.qml ├── Paddings.qml ├── PlaceHolder.qml ├── PlaceholderFont.qml ├── RelativeSize.qml ├── Resource.qml ├── RollerView.qml ├── Sprite.qml └── SwapImage.qml ├── experimental ├── Mosaic.qml ├── MosaicDelegate.qml └── NestedVideo.qml ├── google ├── GoogleChart.qml └── YouTube.qml ├── input ├── BaseInput.qml ├── CheckboxInput.qml ├── ColorInput.qml ├── ComboBoxInput.qml ├── DateInput.qml ├── EmailInput.qml ├── FileInput.qml ├── NumberInput.qml ├── RadioButtonInput.qml ├── RangeInput.qml ├── SearchInput.qml ├── TextAreaInput.qml ├── TextInput.qml └── TimeInput.qml ├── mixins ├── CssMixin.qml ├── DisableHoverByTimeoutMixin.qml ├── DoubleClickMixin.qml ├── DragMixin.qml ├── DropZoneMixin.qml ├── FocusOnHoverMixin.qml ├── HoverClickMixin.qml ├── ImageMixin.qml ├── MouseMixin.qml ├── OrientationMixin.qml ├── OverflowMixin.qml ├── PositionMixin.qml ├── RightClickMixin.qml ├── SwipeMixin.qml ├── TextMixin.qml ├── TitleMixin.qml └── UserSelectMixin.qml ├── package.json ├── pure ├── Checkbox.qml ├── FloatingText.qml ├── MultiLineText.qml ├── ProgressBar.qml ├── ScrollBar.qml ├── Subtitles.qml ├── TextEdit.qml ├── format.js └── keyboard │ ├── Keyboard.qml │ ├── KeyboardDelegate.qml │ ├── KeyboardModel.qml │ └── KeyboardRowModel.qml ├── svg ├── ExternalPath.qml ├── Line.qml ├── Path.qml ├── SvgBase.qml └── SvgContainer.qml ├── web ├── Box.qml ├── Button.qml ├── Canvas.qml ├── CodeHighlighter.qml ├── DataList.qml ├── DropDown.qml ├── ElementWithModel.qml ├── EllipsisText.qml ├── GifSpinner.qml ├── H1.qml ├── H2.qml ├── H3.qml ├── H4.qml ├── H5.qml ├── H6.qml ├── IFrame.qml ├── ImageSet.qml ├── LanguageSelector.qml ├── LanguageSwitcher.qml ├── MaterialIcon.qml ├── Navbar.qml ├── NavbarItem.qml ├── NavigationMenuButton.qml ├── ResizableBox.qml ├── Script.qml ├── Sharer.qml ├── Spinner.qml ├── WebItem.qml ├── WebLink.qml ├── api │ ├── Method.qml │ └── Rest.qml ├── audio │ ├── AudioBuffer.qml │ └── Oscillator.qml ├── gl │ ├── FragmentShader.qml │ ├── GlContext.qml │ ├── Program.qml │ ├── Shader.qml │ └── VertexShader.qml ├── tables │ ├── Table.qml │ ├── TableBody.qml │ ├── TableCell.qml │ ├── TableElement.qml │ ├── TableFooter.qml │ ├── TableHeader.qml │ ├── TableHeaderCell.qml │ └── TableRow.qml └── websocket │ ├── WebSocketClient.qml │ └── WebSocketServer.qml └── yandex └── YandexMap.qml /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = false 5 | 6 | [*] 7 | end_of_line = lf 8 | insert_final_newline = true 9 | charset = utf-8 10 | indent_style = tab 11 | indent_size = 4 12 | trim_trailing_whitespace = true 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | -------------------------------------------------------------------------------- /.manifest: -------------------------------------------------------------------------------- 1 | { "package": "controls", "public": true } 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2015-2017 PureQML Team 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 | -------------------------------------------------------------------------------- /core/Activity.qml: -------------------------------------------------------------------------------- 1 | ///simple activity control 2 | BaseActivity { 3 | ///start activity 4 | start: { 5 | this.active = true 6 | this.started() 7 | } 8 | 9 | ///stop activity 10 | stop: { 11 | this.active = false 12 | this.stopped() 13 | } 14 | 15 | ///get activity item 16 | function getActivity() { 17 | return this 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /core/ActivityManager.qml: -------------------------------------------------------------------------------- 1 | //@using { controls.core.Activity } 2 | //@using { controls.core.LazyActivity } 3 | Item { 4 | property int count; ///< activities count 5 | property bool keepLastActivity: true; ///< allow to keep last activity on screen or not 6 | property string currentActivity; ///< current active activity name 7 | 8 | constructor: { 9 | this._activityStack = [] 10 | } 11 | 12 | /**@param name:string activity name to replace 13 | @param intent:Object activity initial data object 14 | @param state:Object activity initial state object 15 | replace current top activity with new one*/ 16 | replaceTopActivity(name, intent, state): { 17 | if (this.count > 0) { 18 | this._activityStack.pop() 19 | this._activityStack.push({ "name": name, "intent": intent, "state": state }) 20 | this.initTopIntent() 21 | return true 22 | } else { 23 | log("No activity to pop") 24 | return false 25 | } 26 | } 27 | 28 | /**@param name:string activity name to push 29 | @param intent:Object activity initial data object 30 | @param state:Object activity initial state object 31 | push new activity to the top*/ 32 | push(name, intent, state): { 33 | this._activityStack.push({ "name": name, "intent": intent, "state": state }) 34 | this.count++ 35 | this.initTopIntent() 36 | } 37 | 38 | /**@param name:string activity name that must stay 39 | close all activities in stack except 'name' activity*/ 40 | closeAllExcept(name): { 41 | var activity = this.findActivity(name) 42 | 43 | if (activity) { 44 | this.count = 1 45 | this._activityStack = [activity] 46 | } else { 47 | log("Activity", name, "not found, close all") 48 | this.count = 0 49 | this._activityStack = [] 50 | } 51 | if (this.currentActivity != name) 52 | this.initTopIntent() 53 | } 54 | 55 | /**@param count:int activities count to pop from stack 56 | pop top 'count' activities from the stack top*/ 57 | pop(count): { 58 | if ((this.keepLastActivity && this.count > 1) || (!this.keepLastActivity && this.count > 0)) { 59 | var popActivitiesCount = 1 60 | if (count === undefined) 61 | popActivitiesCount = 1 62 | else if (count > this.count - 1 && this.keepLastActivity) 63 | popActivitiesCount = this.count - 1 64 | else if (count > this.count && !this.keepLastActivity) 65 | popActivitiesCount = this.count 66 | else 67 | popActivitiesCount = count 68 | 69 | this._activityStack.splice(-popActivitiesCount, popActivitiesCount) 70 | this.count -= popActivitiesCount 71 | this.initTopIntent() 72 | return true 73 | } else { 74 | log("No activity to pop") 75 | return false 76 | } 77 | } 78 | 79 | /**@param name:string activity name to be removed from stack 80 | remove 'name' activity from stack*/ 81 | removeActivity(name): { 82 | if (name == this.currentActivity) { 83 | this.pop() 84 | } else { 85 | var index = -1 86 | for (var i = 0; i < this._activityStack.length; ++i) { 87 | if (this._activityStack[i].name == name) { 88 | index = i 89 | break 90 | } 91 | } 92 | if (index < 0) { 93 | log("Activity", name, "not found") 94 | return 95 | } 96 | this._activityStack.splice(index, 1) 97 | } 98 | } 99 | 100 | /**@param state:Object activity initial state object 101 | pop top activity and send 'state' to the next activity*/ 102 | popWithState(state): { 103 | if ((this.keepLastActivity && this.count > 1) || (!this.keepLastActivity && this.count > 0)) { 104 | this._activityStack.pop() 105 | --this.count 106 | this.setState(state) 107 | this.initTopIntent() 108 | return true 109 | } else { 110 | log("No activity to pop") 111 | return false 112 | } 113 | } 114 | 115 | /**@param name:string activity name to find 116 | find 'name' activity in stack*/ 117 | findActivity(name): { 118 | var activities = this.children.filter(function(element) { 119 | return element instanceof _globals.controls.core.BaseActivity && element.name == name 120 | }) 121 | 122 | var activity = null 123 | if (activities && activities.length) 124 | activity = activities[0].getActivity() 125 | 126 | if (!activity) 127 | log("Activity for name", name, "not found") 128 | 129 | return activity 130 | } 131 | 132 | ///@private 133 | createActivity(name): { 134 | var activity = this.findActivity(name) 135 | if (activity) 136 | return activity 137 | var activities = this.children.filter(function(element) { 138 | return element instanceof _globals.controls.core.LazyActivity && element.name == name 139 | }) 140 | if (activities && activities.length) { 141 | activity = activities[0] 142 | return activity.createActivity() 143 | } else { 144 | log("Activity for name", name, "not found") 145 | return null 146 | } 147 | } 148 | 149 | /**@param state:Object new state for corresponded activity 150 | @param name:string activity that state will be changed 151 | set state of 'name' activity*/ 152 | setState(state, name): { 153 | if (!name) { 154 | this._activityStack[this._activityStack.length - 1].state = state 155 | } else { 156 | var activity = this.createActivity(name) 157 | if (activity) 158 | activity.state = state 159 | } 160 | } 161 | 162 | /**@param intent:Object new intent for corresponded activity 163 | @param name:string activity that state will be changed 164 | set intent of 'name' activity*/ 165 | setIntent(intent, name): { 166 | if (!name) { 167 | this._activityStack[this._activityStack.length - 1].intent = intent 168 | } else { 169 | var activity = this.createActivity(name) 170 | if (activity) 171 | activity.intent = intent 172 | } 173 | } 174 | 175 | /**@param name:string activity that state will be changed 176 | check is 'name' activity in stack or not*/ 177 | isActivityInStack(name): { 178 | var activities = this._activityStack.filter(function(element) { 179 | return element.name == name 180 | }) 181 | return activities && activities.length > 0 182 | } 183 | 184 | ///clear activities stack 185 | clear: { 186 | var children = this.children 187 | for (var i = 0; i < children.length; ++i) { 188 | var child = children[i] 189 | if (child && child instanceof _globals.controls.core.Activity) 190 | child.stop() 191 | } 192 | this._activityStack = [] 193 | } 194 | 195 | ///@private 196 | initTopIntent: { 197 | try { 198 | this._initTopIntent() 199 | } catch(ex) { 200 | log('initTopIntent failed:', ex) 201 | this.pop() 202 | } 203 | } 204 | 205 | ///@private 206 | _initTopIntent: { 207 | if (!this._activityStack.length) { 208 | log("Activity stack is empty") 209 | return 210 | } 211 | 212 | var topActivity = this._activityStack[this._activityStack.length - 1] 213 | var children = this.children 214 | 215 | log('initTopIntent: ' + topActivity.name) 216 | for (var i = 0; i < children.length; ++i) { 217 | var child = children[i] 218 | if (!child || !(child instanceof _globals.controls.core.BaseActivity)) 219 | continue 220 | 221 | if (child.name === topActivity.name) { 222 | log("Init:", topActivity) 223 | var state = topActivity.state || {} 224 | if (!state.lastActivity) 225 | state.lastActivity = this.currentActivity 226 | child.init(topActivity.intent, state) 227 | child.index = this._activityStack.length - 1 228 | child.start() 229 | child.setFocus() 230 | this.currentActivity = child.name 231 | } else { 232 | child.stop() 233 | } 234 | } 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /core/AnimatedSprite.qml: -------------------------------------------------------------------------------- 1 | /// sprite image animation item 2 | Sprite { 3 | signal finished; ///< animation was ended signal 4 | signal triggered; ///< frame changed signal 5 | property int frameCount; ///< frames in duration value 6 | property int currentFrame; ///< displayed frame index 7 | property int duration; ///< animation time interval in milliseconds 8 | property int interval: duration / frameCount; ///< read only property, time interval between each frames 9 | property bool repeat; ///< repeat animation flag 10 | property bool running; ///< animation is running flag 11 | 12 | /// start animation or continue if paused 13 | start: { this.running = true; } 14 | 15 | /// restarts animation from the beginning 16 | restart: { 17 | this.currentFrame = 0 18 | this.running = true 19 | } 20 | 21 | /// pause animation 22 | pause: { this.running = false; } 23 | 24 | /// stop animation 25 | stop: { 26 | this.currentFrame = 0 27 | this.running = false 28 | } 29 | 30 | ///@private 31 | onCompleted: { 32 | if (this.running) 33 | this._start() 34 | } 35 | 36 | ///@private 37 | onRepeatChanged, 38 | onStatusChanged, 39 | onRunningChanged, 40 | onDurationChanged, 41 | onIntervalChanged, 42 | onFrameCountChanged: { 43 | this._start(); 44 | } 45 | 46 | ///@private 47 | onCurrentFrameChanged: { 48 | var sw = this.sourceWidth, w = this.width 49 | var cols = Math.floor(sw / w) 50 | var col = value % cols 51 | var row = Math.floor(value / cols) 52 | 53 | this.offsetX = col * w 54 | this.offsetY = row * this.height 55 | } 56 | 57 | ///@private 58 | function _start() { 59 | if (this._interval) { 60 | clearInterval(this._interval); 61 | this._interval = undefined; 62 | } 63 | 64 | if (!this.running || this.status != this.Ready) 65 | return; 66 | 67 | var self = this; 68 | 69 | if (self.repeat) 70 | self._interval = setInterval(this._context.wrapNativeCallback(function() { 71 | self.currentFrame = ++self.currentFrame % self.frameCount 72 | self.triggered(); 73 | }), self.interval); 74 | else { 75 | self._countdown = self.frameCount - self.currentFrame 76 | 77 | self._interval = setInterval(this._context.wrapNativeCallback(function() { 78 | if (self._countdown === 0) { 79 | clearInterval(self._interval) 80 | self.running = false 81 | self.finished(); 82 | } 83 | else { 84 | --self._countdown; 85 | self.currentFrame = ++self.currentFrame % self.frameCount 86 | self.triggered(); 87 | } 88 | }), self.interval); 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /core/Audio.qml: -------------------------------------------------------------------------------- 1 | ///COntrol for playing audio 2 | Item { 3 | property bool autoPlay: true; ///= 0) 37 | this.currentIndex = idx 38 | throw new Error("Page " + name + " not found") 39 | } 40 | 41 | /**@param name:string page name 42 | find page index in pagestack by its name*/ 43 | getPageIndexByName(name): { 44 | var pages = this.children 45 | for (var i = 0; i < pages.length; ++i) { 46 | var page = this.children[i] 47 | if (page instanceof _globals.controls.core.LazyPage && page.name == name) 48 | return i 49 | } 50 | return -1 51 | } 52 | 53 | setState(state): { 54 | if (this.count === 0) 55 | throw new Error("There is no activity in stack") 56 | 57 | var currentPage = this.children[this.currentIndex].getPage() 58 | currentPage.state = state 59 | } 60 | 61 | ///set focus on current LazyPage content 62 | chooseCurrentPage: { 63 | if (this._currentPage) 64 | this._currentPage.setFocus() 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /core/Paddings.qml: -------------------------------------------------------------------------------- 1 | /// class controlling internal paddings of the parent element 2 | Object { 3 | property int top: all; ///< top padding 4 | property int left: all; ///< left padding 5 | property int right: all; ///< right padding 6 | property int bottom: all; ///< bottom padding 7 | property int all; ///< a value for all sides 8 | 9 | ///@private 10 | onTopChanged: { this.parent.style('padding-top', value); } 11 | 12 | ///@private 13 | onLeftChanged: { this.parent.style('padding-left', value); } 14 | 15 | ///@private 16 | onRightChanged: { this.parent.style('padding-right', value); } 17 | 18 | ///@private 19 | onBottomChanged: { this.parent.style('padding-bottom', value); } 20 | } 21 | -------------------------------------------------------------------------------- /core/PlaceHolder.qml: -------------------------------------------------------------------------------- 1 | ///placeholder adjustment object for inputs 2 | Object { 3 | property string text; ///< inner text placeholder 4 | property Color color; ///< placeholder color 5 | property Font font: PlaceholderFont { } ///< placeholder font 6 | 7 | ///@private 8 | constructor: { this._placeholderClass = '' } 9 | 10 | ///@private 11 | onTextChanged: { this.parent.element.setAttribute('placeholder', value) } 12 | 13 | ///@private 14 | onColorChanged: { this.setPlaceholderColor(value) } 15 | 16 | ///@private 17 | function getClass() { 18 | var cls 19 | if (!this._placeholderClass) { 20 | cls = this._placeholderClass = this._context.stylesheet.allocateClass('input') 21 | this.parent.element.addClass(cls) 22 | } 23 | else 24 | cls = this._placeholderClass 25 | return cls 26 | } 27 | 28 | ///@private 29 | function setPlaceholderColor(color) { 30 | var cls = this.getClass() 31 | 32 | var rgba = $core.Color.normalize(color) 33 | this.parent.element.style('-pure-placeholder-color', rgba) 34 | 35 | //fixme: port to modernizr 36 | var selectors = ['::-webkit-input-placeholder', '::-moz-placeholder', ':-moz-placeholder', ':-ms-input-placeholder'] 37 | selectors.forEach(function(selector) { 38 | this._context.stylesheet.addRule('.' + cls + selector, {'color': rgba}) 39 | }.bind(this)) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /core/PlaceholderFont.qml: -------------------------------------------------------------------------------- 1 | Object { 2 | property string family; ///< font family 3 | property bool italic; ///< applies italic style 4 | property bool bold; ///< applies bold style 5 | property bool underline; ///< applies underline style 6 | property bool strike; ///< line throw text flag 7 | property int pixelSize; ///< font size in pixels 8 | property int pointSize; ///< font size in points 9 | property int lineHeight; ///< font line height in pixels 10 | property int weight; ///< font weight value 11 | 12 | /// @private 13 | onBoldChanged: { this.updateProperty('font-weight', value? 'bold': 'normal'); this.parent.parent._updateSize(); } 14 | 15 | /// @private 16 | onWeightChanged: { this.updateProperty('font-weight', value); this.parent.parent._updateSize(); } 17 | 18 | /// @private 19 | onFamilyChanged: { this.updateProperty('font-family', value); this.parent.parent._updateSize(); } 20 | 21 | /// @private 22 | onItalicChanged: { this.updateProperty('font-style', value? 'italic': 'normal'); this.parent.parent._updateSize(); } 23 | 24 | /// @private 25 | onStrikeChanged: { this.updateProperty('text-decoration', value? 'line-through': ''); this.parent._updateSize(); } 26 | 27 | /// @private 28 | onPointSizeChanged: { this.updateProperty('font-size', value + "pt"); this.parent.parent._updateSize(); } 29 | 30 | /// @private 31 | onPixelSizeChanged: { this.updateProperty('font-size', value + "px"); this.parent.parent._updateSize(); } 32 | 33 | /// @private 34 | onUnderlineChanged: { this.updateProperty('text-decoration', value? 'underline': ''); this.parent.parent._updateSize(); } 35 | 36 | /// @private 37 | onLineHeightChanged: { this.updateProperty('line-height', value + "px"); this.parent.parent._updateSize(); } 38 | 39 | /// @private 40 | function getClass() { 41 | var cls 42 | if (!this._placeholderClass) { 43 | cls = this._placeholderClass = this._context.stylesheet.allocateClass('placeholderFont') 44 | this.parent.parent.element.addClass(cls) 45 | } 46 | else 47 | cls = this._placeholderClass 48 | return cls 49 | } 50 | 51 | /// @private 52 | function updateProperty(name, value) { 53 | var cls = this.getClass() 54 | 55 | //fixme: port to modernizr 56 | var selectors = ['::-webkit-input-placeholder', '::-moz-placeholder', ':-moz-placeholder', ':-ms-input-placeholder'] 57 | selectors.forEach(function(selector) { 58 | try { 59 | this._context.stylesheet.addRule('.' + cls + selector, {name: value}) 60 | log('added rule for .' + cls + selector) 61 | } catch(ex) { 62 | //log(ex) 63 | } 64 | }.bind(this)) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /core/RelativeSize.qml: -------------------------------------------------------------------------------- 1 | ///object for geting relative size values 2 | Object { 3 | property real xs: 100; ///< extra small devices, phones (<768px) 4 | property real sm: 0; ///< small devices, tablets (>=768px) 5 | property real md: 0; ///< medium devices, desktops (>=992px) 6 | property real lg: 0; ///< large devices, desktops (>=1200px) 7 | property int globalWidth: context.width; ///<@private 8 | 9 | onGlobalWidthChanged: { this._calculate() } 10 | 11 | ////@private 12 | function _calculate() { 13 | var gw = this.globalWidth; 14 | if (gw > 1200) 15 | this.parent.width = this.parent.parent.width * (this.lg || this.md || this.sm || this.xs) / 100 16 | else if (gw > 992) 17 | this.parent.width = this.parent.parent.width * (this.md || this.sm || this.xs) / 100 18 | else if (gw > 768) 19 | this.parent.width = this.parent.parent.width * (this.sm || this.xs) / 100 20 | else 21 | this.parent.width = this.parent.parent.width * this.xs / 100 22 | } 23 | 24 | onCompleted: { this._calculate() } 25 | } 26 | -------------------------------------------------------------------------------- /core/Resource.qml: -------------------------------------------------------------------------------- 1 | /// Object for getting remote resources 2 | Request { 3 | property string url; ///= 400) 16 | self.error(data) 17 | else 18 | self.data = target.responseText 19 | }, 20 | error: function(err) { self.error(err) } 21 | }) 22 | } else { 23 | this.data = '' 24 | } 25 | } 26 | 27 | onUrlChanged: { this.load(value) } 28 | onCompleted: { this.load(this.url) } 29 | } 30 | -------------------------------------------------------------------------------- /core/RollerView.qml: -------------------------------------------------------------------------------- 1 | BaseView { 2 | property enum orientation { Vertical, Horizontal }: Horizontal; //vertical was not widely tested 3 | content.cssDelegateAlwaysVisibleOnAcceleratedSurfaces: false; 4 | 5 | constructor: { 6 | this._oldIndex = 0 7 | this._nextDelta = 0 8 | } 9 | 10 | /// @private 11 | function positionViewAtIndex(idx) { } 12 | 13 | /// @private 14 | function _getCurrentIndex(adj) { 15 | var n = this._items.length 16 | if (adj === undefined) 17 | adj = 0 18 | return (((this.currentIndex + adj) % n) + n) % n 19 | } 20 | 21 | /// @private creates delegate in given item slot 22 | function _createDelegate(idx) { 23 | var items = this._items 24 | var item = items[idx] 25 | if (item !== null && item !== undefined) 26 | return item 27 | 28 | var item = _globals.core.BaseView.prototype._createDelegate.apply(this, arguments) 29 | if (this.orientation === this.Horizontal) 30 | item.onChanged('width', this._scheduleLayout.bind(this)) 31 | else 32 | item.onChanged('height', this._scheduleLayout.bind(this)) 33 | return item 34 | } 35 | 36 | /// @private 37 | function _layout(noPrerender) { 38 | var model = this._modelAttached; 39 | if (!model) 40 | return 41 | 42 | this.count = model.count 43 | 44 | if (!this.recursiveVisible && !this.offlineLayout) { 45 | this.layoutFinished() 46 | return 47 | } 48 | 49 | var horizontal = this.orientation === this.Horizontal 50 | var items = this._items 51 | 52 | if (!items.length || !this.count) { 53 | this.layoutFinished() 54 | return 55 | } 56 | 57 | var view = this 58 | var w = this.width 59 | var h = this.height 60 | var created = false 61 | var size = horizontal ? w : h 62 | var n = items.length 63 | var currentIndex = this._getCurrentIndex() 64 | var spacing = this.spacing 65 | 66 | var positionMode = this.positionMode 67 | 68 | var currentItem = this._createDelegate(currentIndex) 69 | var currentItemSize = horizontal? currentItem.width: currentItem.height 70 | 71 | var prerender = noPrerender? 0: this.prerender * size 72 | var leftViewMargin = -currentItemSize 73 | var rightViewMargin = size + currentItemSize 74 | var leftMargin = leftViewMargin - prerender 75 | var rightMargin = rightViewMargin + prerender 76 | 77 | var pos 78 | switch(positionMode) { 79 | case this.Beginning: 80 | pos = 0 81 | break 82 | case this.End: 83 | pos = size - currentItemSize 84 | break 85 | default: 86 | //log('unsupported position mode ' + positionMode) 87 | case this.Center: 88 | pos = (size - currentItemSize) / 2 89 | break 90 | } 91 | 92 | if (horizontal) 93 | currentItem.viewX = pos 94 | else 95 | currentItem.viewY = pos 96 | 97 | if (this.trace) 98 | log("layout " + n + " into " + w + "x" + h + " @ " + this.content.x + "," + this.content.y + ", prerender: " + prerender + ", range: " + leftMargin + ":" + rightMargin) 99 | 100 | for(var i = 0; i < n; ++i) { 101 | var item = items[i] 102 | if (item) 103 | item.__rendered = false 104 | } 105 | 106 | var leftInPrerender = true, rightInPrerender = true, leftInView = true, rightInView = true 107 | var prevRight = pos, prevLeft = pos 108 | var nextLeftIndex = -1, nextRightIndex = 0 109 | 110 | var positionItem = function(idx, item, itemPos) { 111 | item.visibleInView = true 112 | if (horizontal) 113 | item.viewX = itemPos 114 | else 115 | item.viewY = itemPos 116 | 117 | if (currentIndex == idx && !item.focused) { 118 | view.focusChild(item) 119 | if (view.contentFollowsCurrentItem) 120 | view.positionViewAtIndex(i) 121 | } 122 | } 123 | 124 | var positionLeft = function() { 125 | var idx = view._getCurrentIndex(nextLeftIndex) 126 | var item = view._createDelegate(idx) 127 | if (item.__rendered) 128 | return false 129 | 130 | --nextLeftIndex 131 | 132 | var itemSize = horizontal? item.width: item.height 133 | var itemPos = prevLeft - spacing - itemSize 134 | 135 | if (itemPos + itemSize <= leftMargin) 136 | leftInPrerender = false 137 | if (itemPos + itemSize <= leftViewMargin) 138 | leftInView = false 139 | prevLeft = itemPos 140 | item.__rendered = true 141 | positionItem(idx, item, itemPos) 142 | if (view.trace) 143 | log('positioned (left) ', idx, 'at', itemPos) 144 | return true 145 | } 146 | 147 | var positionRight = function() { 148 | var idx = view._getCurrentIndex(nextRightIndex) 149 | var item = view._createDelegate(idx) 150 | if (item.__rendered) 151 | return false 152 | 153 | ++nextRightIndex 154 | 155 | var itemSize = horizontal? item.width: item.height 156 | var itemPos = prevRight 157 | 158 | if (itemPos >= rightMargin) 159 | rightInPrerender = false 160 | if (itemPos >= rightViewMargin) 161 | rightInView = false 162 | prevRight = itemPos + itemSize + spacing 163 | item.__rendered = true 164 | positionItem(idx, item, itemPos) 165 | if (view.trace) 166 | log('positioned (right) ', idx, 'at', itemPos) 167 | return true 168 | } 169 | 170 | positionRight() //first element 171 | 172 | while(leftInView || rightInView) { 173 | if (leftInView && !positionLeft()) 174 | leftInView = false 175 | if (rightInView && !positionRight()) 176 | rightInView = false 177 | } 178 | 179 | while(leftInPrerender || rightInPrerender) { 180 | if (leftInPrerender && !positionLeft()) 181 | leftInPrerender = false 182 | if (rightInPrerender && !positionRight()) 183 | rightInPrerender = false 184 | } 185 | 186 | for(var i = 0; i < n; ++i) { 187 | var item = items[i] 188 | if (item && !item.__rendered) 189 | item.visibleInView = false 190 | } 191 | 192 | 193 | var nextDelta = this._nextDelta 194 | this._nextDelta = 0 195 | if (nextDelta !== 0) { 196 | //disable animation 197 | var animationDuration = this.animationDuration 198 | this.animationDuration = 0 199 | //set offset without layout 200 | this._setContentOffset(-nextDelta) 201 | 202 | //update everything 203 | this.content.element.forceLayout() 204 | //enable animation 205 | this.animationDuration = animationDuration 206 | //simulate animation to 0 207 | this._setContentOffset(0) 208 | } 209 | this._context.scheduleComplete() 210 | } 211 | 212 | /// @private 213 | function next() { 214 | var n = this._items.length 215 | if (n > 1) 216 | this.currentIndex = this._getCurrentIndex(1) 217 | } 218 | 219 | /// @private 220 | function prev() { 221 | var n = this._items.length 222 | if (n > 1) 223 | this.currentIndex = this._getCurrentIndex(-1) 224 | } 225 | 226 | onKeyPressed: { 227 | if (!this.handleNavigationKeys) 228 | return false; 229 | 230 | var horizontal = this.orientation == this.Horizontal 231 | if (horizontal) { 232 | if (key == 'Left') { 233 | this.prev() 234 | return true; 235 | } else if (key == 'Right') { 236 | this.next() 237 | return true; 238 | } 239 | } else { 240 | if (key == 'Up') { 241 | this.prev(); 242 | return true; 243 | } else if (key == 'Down') { 244 | this.next(); 245 | return true; 246 | } 247 | } 248 | } 249 | 250 | /// @private 251 | function _setContentOffset(offset) { 252 | if (this.orientation === this.Horizontal) { 253 | this._setProperty('contentX', offset) 254 | this.content._setProperty('x', -offset) 255 | } else { 256 | this._setProperty('contentY', offset) 257 | this.content._setProperty('y', -offset) 258 | } 259 | } 260 | 261 | /// @private 262 | function _scroll(currentIndex, oldIndex, delta) { 263 | var prevItem = this._items[oldIndex] 264 | var item = this._items[currentIndex] 265 | if (!item || !item.visibleInView || !prevItem || !prevItem.visibleInView) { 266 | this._scheduleLayout() 267 | return 268 | } 269 | 270 | var horizontal = this.orientation === this.Horizontal 271 | //log('scrolling to ', currentIndex, oldIndex, item.viewX, delta) 272 | 273 | //fixme: allow less frequent layouts 274 | //if (item.viewX < 0 || (item.viewX + item.width) > this.width) 275 | this._scheduleLayout() 276 | //else 277 | // this._setContentOffset(this.contentX + this._nextDelta) 278 | 279 | this._nextDelta = delta * ((horizontal? prevItem.width: prevItem.height) + this.spacing) 280 | } 281 | 282 | onOrientationChanged: { this._scheduleLayout() } 283 | 284 | onCurrentIndexChanged: { 285 | var oldIndex = this._oldIndex 286 | if (value !== oldIndex) { 287 | var n = this._items.length 288 | var m = n / 2 289 | var delta = value - oldIndex 290 | if (delta > m) 291 | delta = delta - n 292 | if (delta < -m) 293 | delta = delta + n 294 | 295 | //log('currentIndexChanged', value, oldIndex, delta) 296 | this._scroll(value, oldIndex, delta) 297 | this._oldIndex = value 298 | } 299 | } 300 | } 301 | -------------------------------------------------------------------------------- /core/Sprite.qml: -------------------------------------------------------------------------------- 1 | /// base sprite object 2 | Image { 3 | property int offsetX; ///< sprite horizontal offset 4 | property int offsetY; ///< sprite vertical offset 5 | fillMode: Image.Tile; ///< sprite image filling mode 6 | horizontalAlignment: Image.AlignLeft; 7 | verticalAlignment: Image.AlignTop; 8 | 9 | onOffsetYChanged, 10 | onOffsetXChanged: { 11 | this.style('background-position', '-' + this.offsetX + 'px -' + this.offsetY + 'px'); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /core/SwapImage.qml: -------------------------------------------------------------------------------- 1 | Item { 2 | id: swapImageProto; 3 | property int swapDuration: 500; 4 | property enum fillMode { Stretch, PreserveAspectFit, PreserveAspectCrop, Tile, TileVertically, TileHorizontally, Pad }; 5 | 6 | Image { 7 | id: backImage; 8 | anchors.fill: parent; 9 | fillMode: parent.fillMode; 10 | 11 | onStatusChanged: { 12 | if (frontImage.atTheTop && value == this.Ready) { 13 | frontImage.opacity = 0.0 14 | animationSyncTimer.restart() 15 | } 16 | } 17 | 18 | Behavior on opacity { Animation { duration: swapImageProto.swapDuration; } } 19 | } 20 | 21 | Image { 22 | id: frontImage; 23 | property bool atTheTop: z == parent.z; 24 | anchors.fill: parent; 25 | fillMode: parent.fillMode; 26 | 27 | onStatusChanged: { 28 | if (!frontImage.atTheTop && value == this.Ready) { 29 | backImage.opacity = 0.0 30 | animationSyncTimer.restart() 31 | } 32 | } 33 | 34 | Behavior on opacity { Animation { duration: swapImageProto.swapDuration; } } 35 | } 36 | 37 | Timer { 38 | id: animationSyncTimer; 39 | interval: swapImageProto.swapDuration; 40 | 41 | onTriggered: { this.parent.swapLayers() } 42 | } 43 | 44 | swapLayers: { 45 | if (frontImage.atTheTop) { 46 | frontImage.z = this.z - 1 47 | backImage.z = this.z 48 | frontImage.opacity = 1.0 49 | } else { 50 | backImage.z = this.z - 1 51 | frontImage.z = this.z 52 | backImage.opacity = 1.0 53 | } 54 | } 55 | 56 | swap(source): { 57 | if (!frontImage.source) { 58 | backImage.z = this.z - 1 59 | frontImage.z = this.z 60 | frontImage.source = source 61 | } else if (frontImage.atTheTop) { 62 | backImage.source = source 63 | } else { 64 | frontImage.source = source 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /experimental/Mosaic.qml: -------------------------------------------------------------------------------- 1 | GridView { 2 | id: mosaicGrid; 3 | property bool playing; 4 | property bool hoverMode; 5 | property bool mobile: context.system.device === context.system.Mobile; 6 | property int offset; 7 | signal play; 8 | signal itemFocused; 9 | property int delegateRadius; 10 | width: 100%; 11 | height: 100%; 12 | cellWidth: (context.width > context.height ? 0.25 : 0.5) * (width - padding.left - padding.right) - spacing; 13 | cellHeight: cellWidth * 0.5625; 14 | spacing: 10s; 15 | keyNavigationWraps: false; 16 | content.cssTranslatePositioning: true; 17 | contentFollowsCurrentItem: !hoverMode; 18 | model: ListModel { } 19 | delegate: MosaicDelegate { allowOpacity: parent.playing; } 20 | padding: 15s; 21 | onKeyPressed: { 22 | this.hoverMode = false 23 | return false 24 | } 25 | 26 | fill(items, mappingFunc): { 27 | var res = [] 28 | for (var i in items) { 29 | var row = mappingFunc(items[i]) 30 | if (row) 31 | res.push(row) 32 | } 33 | this.model.assign(res) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /experimental/MosaicDelegate.qml: -------------------------------------------------------------------------------- 1 | Rectangle { 2 | signal pressed; 3 | property bool allowOpacity; 4 | property bool playing; 5 | width: parent.cellWidth; 6 | height: parent.cellHeight; 7 | property bool active: activeFocus; 8 | property Mixin hoverMixin: HoverClickMixin { } 9 | property alias hover: hoverMixin.value; 10 | anchors.margins: mosaicGrid.delegateRadius; 11 | transform.scaleX: active ? 1.05 : 1; 12 | transform.scaleY: active ? 1.05 : 1; 13 | effects.shadow.blur: 10; 14 | effects.shadow.color: active ? "#8AF" : "#0000"; 15 | effects.shadow.spread: 2; 16 | border.width: active ? 1 : 0; 17 | border.color: active ? "#8AF" : "#0000"; 18 | radius: mosaicGrid.delegateRadius; 19 | color: "#464646"; 20 | clip: true; 21 | opacity: playing && allowOpacity ? 0.0 : 1.0; 22 | z: active ? parent.z + 1 : parent.z; 23 | 24 | MouseMoveMixin { 25 | onMouseMove: { 26 | mosaicGrid.hoverMode = true 27 | mosaicGrid.currentIndex = model.index 28 | } 29 | } 30 | 31 | Image { 32 | id: programImage; 33 | property bool display; 34 | source: model.preview? model.preview: ''; 35 | anchors.fill: parent; 36 | fillMode: Image.PreserveAspectCrop; 37 | visible: source; 38 | 39 | onStatusChanged: { 40 | this.display = this.status == this.Ready 41 | } 42 | } 43 | 44 | Rectangle { 45 | width: 100%; 46 | height: 70s; 47 | anchors.bottom: parent.bottom; 48 | anchors.bottomMargin: 2s; 49 | gradient: Gradient { 50 | GradientStop { color: "#0000"; position: 0.0; } 51 | GradientStop { color: "#000"; position: 1.0; } 52 | } 53 | } 54 | 55 | Image { 56 | x: 10s; 57 | width: 100s; 58 | height: 70s; 59 | anchors.bottom: parent.bottom; 60 | anchors.bottomMargin: 21s; 61 | fillMode: Image.PreserveAspectFit; 62 | source: model.icon? model.icon: ''; 63 | verticalAlignment: Image.AlignTop; 64 | horizontalAlignment: Image.AlignRight; 65 | } 66 | 67 | EllipsisText { 68 | x: 10s; 69 | width: 270s; 70 | anchors.bottom: parent.bottom; 71 | anchors.bottomMargin: 6s; 72 | font.pixelSize: mosaicGrid.mobile ? 9s : 18s; 73 | color: "#fff"; 74 | text: model.title; 75 | } 76 | 77 | Rectangle { 78 | width: 100%; 79 | height: 2s; 80 | color: "#000c"; 81 | anchors.bottom: parent.bottom; 82 | clip: true; 83 | 84 | Rectangle { 85 | height: 100%; 86 | width: parent.width * model.progress; 87 | color: "#e53935"; 88 | } 89 | } 90 | 91 | Timer { 92 | id: flipTimer; 93 | interval: 3000; 94 | 95 | onTriggered: { 96 | if (!this.parent.active) 97 | return 98 | this.parent.playing = true 99 | mosaicGrid.itemFocused(model.index) 100 | } 101 | } 102 | 103 | onActiveChanged: { 104 | if (!mosaicGrid._firstTimeFlag) { 105 | mosaicGrid._firstTimeFlag = true 106 | return 107 | } 108 | 109 | if (!value) { 110 | this.playing = false 111 | return 112 | } 113 | 114 | flipTimer.restart() 115 | } 116 | 117 | onClicked: { mosaicGrid.currentIndex = model.index; this.pressed() } 118 | onSelectPressed: { this.pressed() } 119 | onPressed: { mosaicGrid.play(model.index) } 120 | 121 | Behavior on opacity { Animation { duration: 1200; } } 122 | Behavior on transform, boxshadow { Animation { duration: 400; } } 123 | } 124 | -------------------------------------------------------------------------------- /experimental/NestedVideo.qml: -------------------------------------------------------------------------------- 1 | Rectangle { 2 | property bool display; 3 | transform.scaleX: display ? 1.05 : 1.0; 4 | transform.scaleY: display ? 1.05 : 1.0; 5 | visible: false; 6 | clip: true; 7 | color: "transparent"; 8 | 9 | VideoPlayer { 10 | id: videoPlayer; 11 | anchors.fill: parent; 12 | autoPlay: true; 13 | } 14 | 15 | showAndPlay(source): { 16 | log('VideoPlayer showAndPlay', source) 17 | this.visible = true 18 | this.display = true 19 | videoPlayer.source = source 20 | } 21 | 22 | hide: { 23 | log('VideoPlayer hide') 24 | this.visible = false 25 | this.display = false 26 | videoPlayer.source = "" 27 | videoPlayer.stop() 28 | } 29 | 30 | Behavior on transform { Animation { duration: 400; } } 31 | } 32 | -------------------------------------------------------------------------------- /google/GoogleChart.qml: -------------------------------------------------------------------------------- 1 | /// google chart item 2 | Item { 3 | property enum type { Pie, Line, Column }; ///< chart type enum Pie, Line or Column 4 | 5 | /**@param data:Object data for chart building 6 | @param opt:Object chart displaying properties 7 | Fill google chart function*/ 8 | fill(data, opt): { 9 | var elem = this.element 10 | var w = this.width 11 | var h = this.height 12 | var self = this 13 | google.charts.load("current", {packages:['corechart']}); 14 | google.charts.setOnLoadCallback(function() { 15 | if (!data) 16 | return 17 | 18 | var visData = google.visualization.arrayToDataTable(data) 19 | var view = new google.visualization.DataView(visData); 20 | var options = opt || [] 21 | var chart 22 | switch (self.type) { 23 | case self.Pie: chart = new google.visualization.PieChart(elem.dom); break 24 | case self.Line: chart = new google.visualization.LineChart(elem.dom); break 25 | default: chart = new google.visualization.ColumnChart(elem.dom); break 26 | } 27 | chart.draw(view, options); 28 | }); 29 | } 30 | 31 | /// @private 32 | function registerStyle(style) { 33 | style.addRule('svg', { 34 | "position": "absolute", 35 | "visibility": "inherit", 36 | "border-style": "solid", 37 | "border-width": "0px", 38 | "box-sizing": "border-box" 39 | }) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /google/YouTube.qml: -------------------------------------------------------------------------------- 1 | ///item for embedded youtube video 2 | Item { 3 | property string source; ///< video source URL 4 | property bool allowFullScreen; ///< allow fullscreen flag 5 | height: 315; ///<@private 6 | width: 560; ///<@private 7 | 8 | ///@private 9 | onWidthChanged, 10 | onHeightChanged: { this._updateSize(); } 11 | 12 | ///@private 13 | onSourceChanged: { this.element.dom.src = value; } 14 | 15 | ///@private 16 | onAllowFullScreenChanged: { this.element.dom.allowFullscreen = value; } 17 | 18 | ///@private 19 | function getTag() { return 'iframe' } 20 | 21 | ///@private 22 | function _updateSize() { 23 | var style = { width: this.width, height: this.height } 24 | this.style(style) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /input/BaseInput.qml: -------------------------------------------------------------------------------- 1 | ///base class for all inputs 2 | Item { 3 | property enum horizontalAlignment { AlignLeft, AlignRight, AlignHCenter, Justify }; ///< inner text alignment 4 | property lazy paddings: Paddings {} ///< inner text paddings 5 | property Color color: "#000"; ///< text color 6 | property Color backgroundColor: "#fff"; ///< background color 7 | property Font font: Font {} ///< object holding properties of text font 8 | property Border border: Border {} ///< object holding properties of the border 9 | property string type: "text"; ///< input type value, must override in inheriting component 10 | property PlaceHolder placeholder: PlaceHolder{} ///< input placeholder object 11 | property bool enabled: true; ///< input enabled 12 | property bool nativeFocus: manifest.useNativeFocusForInput; ///< use native focus for input (may trigger IME) 13 | property string inputMode; ///< inputmode attribute, numeric keyboard, etc 14 | property string autocomplete; ///< autocomplete variants (username, current-password, etc) 15 | signal change; ///< emit signal when input loses focus or IME closes 16 | cssPointerTouchEvents: true; 17 | 18 | /// @private 19 | constructor: { 20 | this._placeholderClass = '' 21 | this.element.on("focus", function() { this.forceActiveFocus(); }.bind(this)) 22 | this.element.on("blur", function() { /* fixme: remove focus from current input */ }.bind(this)) 23 | this.element.on("change", function() { this.change() }.bind(this)) 24 | } 25 | 26 | /// @private 27 | function _setId(name) { 28 | this.element.setAttribute("name", name) 29 | $core.Item.prototype._setId.apply(this, arguments) 30 | } 31 | 32 | /// @private 33 | onActiveFocusChanged: { 34 | if (value) 35 | this.focusBrowser() 36 | else 37 | this.blurBrowser() 38 | } 39 | 40 | /// @private 41 | onRecursiveVisibleChanged: { 42 | if (!value) 43 | this.blurBrowser() 44 | } 45 | 46 | /// @private 47 | onWidthChanged, 48 | onHeightChanged: { this._updateSize() } 49 | 50 | /// @private 51 | onTypeChanged: { this.element.setAttribute('type', value) } 52 | 53 | /// @private 54 | onColorChanged: { this.style('color', value) } 55 | 56 | /// @private 57 | onBackgroundColorChanged: { this.style('background', value) } 58 | 59 | /// @private 60 | onHorizontalAlignmentChanged: { 61 | switch(value) { 62 | case this.AlignLeft: this.style('text-align', 'left'); break 63 | case this.AlignRight: this.style('text-align', 'right'); break 64 | case this.AlignHCenter: this.style('text-align', 'center'); break 65 | case this.AlignJustify: this.style('text-align', 'justify'); break 66 | } 67 | } 68 | 69 | onEnabledChanged: { 70 | if(value) { 71 | this.element.removeAttribute('disabled'); 72 | } 73 | else { 74 | this.element.setAttribute('disabled', true); 75 | } 76 | } 77 | 78 | onAutocompleteChanged: { 79 | this.element.setAttribute('autocomplete', value) 80 | } 81 | 82 | /// returns tag for corresponding element 83 | function getTag() { return 'input' } 84 | 85 | /// @private 86 | function registerStyle(style) { 87 | style.addRule('input', { 88 | "position": "absolute", 89 | "visibility": "inherit", 90 | "border-style": "solid", 91 | "border-width": "0px", 92 | "box-sizing": "border-box" 93 | }) 94 | style.addRule('input:focus', {"outline": "none"}) 95 | } 96 | 97 | /// @private 98 | function _updateSize() { 99 | var style = { width: this.width, height: this.height } 100 | this.style(style) 101 | } 102 | 103 | /// focus browser 104 | function focusBrowser() { 105 | if (!this.nativeFocus) 106 | return 107 | 108 | focusTimer.restart() 109 | } 110 | 111 | /// blur browser 112 | function blurBrowser() { 113 | if (!this.nativeFocus) 114 | return 115 | 116 | focusTimer.stop() 117 | this.element.blur() 118 | } 119 | 120 | /// gets element native value 121 | function _getValue() { 122 | return this.element.getProperty('value') 123 | } 124 | 125 | /// sets element native value 126 | function _setValue(value) { 127 | this.element.setProperty('value', value) 128 | } 129 | 130 | /// @private 131 | function _updateValue(value) { 132 | if (value !== this._getValue()) 133 | this._setValue(value) 134 | } 135 | 136 | onInputModeChanged: { 137 | this.element.setAttribute('inputmode', value) 138 | } 139 | 140 | Timer { 141 | id: focusTimer; 142 | interval: 100; 143 | 144 | onTriggered: { 145 | this.parent.element.focus() 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /input/CheckboxInput.qml: -------------------------------------------------------------------------------- 1 | /// web browser checkbox 2 | BaseInput { 3 | property bool checked: false; ///< checked flag value 4 | height: 20; 5 | width: 20; 6 | type: "checkbox"; 7 | 8 | onCheckedChanged: { 9 | this.element.setProperty('checked', value) 10 | } 11 | 12 | constructor: { 13 | this.element.on("change", function() { 14 | this.checked = this.element.getProperty('checked') 15 | }.bind(this)) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /input/ColorInput.qml: -------------------------------------------------------------------------------- 1 | ///color selecting input 2 | BaseInput { 3 | width: 80; 4 | height: 32; 5 | type: "color"; 6 | 7 | onColorChanged: { 8 | this._updateValue(value) 9 | } 10 | 11 | constructor: { 12 | this.element.on("input", function(e) { this.color = e.target.value; }.bind(this)) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /input/ComboBoxInput.qml: -------------------------------------------------------------------------------- 1 | TextInput { 2 | property bool trace: false; 3 | property Model model; 4 | property string valueProperty: "value"; 5 | 6 | DataList { 7 | model: parent.model; 8 | trace: parent.trace; 9 | valueProperty: parent.valueProperty; 10 | 11 | onCompleted: { 12 | var element = this.element 13 | var input = this.parent 14 | element.remove() 15 | input.parent.element.append(element) 16 | input.element.setAttribute("list", this.domId) 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /input/DateInput.qml: -------------------------------------------------------------------------------- 1 | ///date selecting input 2 | BaseInput { 3 | property string max; ///< maximum date value 4 | property string min; ///< minimal date value 5 | property string value; ///< current date value 6 | width: 150; 7 | height: 20; 8 | type: "date"; 9 | 10 | onMinChanged: { this.element.setProperty('min', value) } 11 | onMaxChanged: { this.element.setProperty('max', value) } 12 | 13 | constructor: { 14 | this.element.on("change", function() { 15 | this.value = this._getValue() 16 | }.bind(this)) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /input/EmailInput.qml: -------------------------------------------------------------------------------- 1 | ///email input 2 | TextInput { 3 | property bool valid; ///< is typed email valid or not flag 4 | type: "email"; 5 | 6 | onTextChanged: { 7 | if (!this.re) 8 | this.re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; 9 | this.valid = this.re.test(value); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /input/FileInput.qml: -------------------------------------------------------------------------------- 1 | ///file selected input 2 | BaseInput { 3 | property string value; ///< selected filename 4 | property string filter; ///< filter for file selecting 5 | width: 250; 6 | height: 30; 7 | type: "file"; 8 | 9 | onFilterChanged: { this.element.setProperty('accept', value) } 10 | 11 | constructor: { 12 | var self = this 13 | this.element.on("change", function(e) { self.value = e.target.value }.bind(this)) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /input/NumberInput.qml: -------------------------------------------------------------------------------- 1 | ///number input 2 | BaseInput { 3 | property float max; ///< maximum number value 4 | property float min; ///< minimum number value 5 | property float step; ///< number step value 6 | property float value; ///< number value - suggested to use onValueChanged not onChange signal in instantiated component 7 | property float debounceTimeout: 0; ///< set to non zero to debounce input allowing user time to type characters (suggest 500) 8 | horizontalAlignment: BaseInput.AlignHCenter; 9 | width: 50; 10 | height: 25; 11 | type: "number"; 12 | 13 | onMinChanged: { this.element.setProperty('min', value) } 14 | onMaxChanged: { this.element.setProperty('max', value) } 15 | onStepChanged: { this.element.setProperty('step', value) } 16 | onValueChanged: { this.value = this._setValueWithLimits(value); } 17 | 18 | Timer { 19 | id: debounceTimer; 20 | interval: parent.debounceTimeout; 21 | property float value; 22 | triggeredOnStart: false; 23 | 24 | onTriggered: { 25 | this.parent.value = this.parent._setValueWithLimits(this.value); 26 | } 27 | } 28 | 29 | /// @private - sets element native value while applying min/max limits. if called externally, onValueChanged will not fire 30 | function _setValueWithLimits(_value) { 31 | if(_value > this.max) { 32 | _value = this.max; 33 | } else if(_value < this.min) { 34 | _value = this.min; 35 | } 36 | this._setValue(_value); 37 | return(_value); 38 | } 39 | 40 | constructor: { 41 | this.element.on("input", function() { 42 | this._local.debounceTimer.value = parseFloat(this._getValue()); 43 | this._local.debounceTimer.restart(); 44 | }.bind(this)) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /input/RadioButtonInput.qml: -------------------------------------------------------------------------------- 1 | /// web browser RadioInput - change event is emitted when selected 2 | BaseInput { 3 | property string groupName: ""; 4 | property string optionId: ""; 5 | height: 20; 6 | width: 20; 7 | type: "radio"; 8 | 9 | onGroupNameChanged: { 10 | this.element.setAttribute("name", this.groupName); 11 | } 12 | onOptionIdChanged: { 13 | this.element.setAttribute("id", this.optionId); 14 | } 15 | constructor: { 16 | if("" !== this.groupName) { 17 | this.element.setAttribute("name", this.groupName); 18 | } 19 | if("" !== this.optionId) { 20 | this.element.setAttribute("id", this.optionId); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /input/RangeInput.qml: -------------------------------------------------------------------------------- 1 | ///range input 2 | Item { 3 | property int value; ///< current value 4 | property int min: 0; ///< minimal range value 5 | property int max: 100; ///< maximum range value 6 | property int step: 1; ///< range step 7 | property enum orientation { Horizontal, Vertical }; ///< range position orientation 8 | height: 30; 9 | 10 | /// @private 11 | constructor: { 12 | this.element.setAttribute('type', 'range') 13 | this._setValue(0) 14 | this.element.on("input", function() { 15 | this.value = this._getValue() 16 | }.bind(this)) 17 | } 18 | 19 | /// @private 20 | onMinChanged: { this.element.setProperty('min', value) } 21 | 22 | /// @private 23 | onMaxChanged: { this.element.setProperty('max', value) } 24 | 25 | /// @private 26 | onStepChanged: { this.element.setProperty('step', value) } 27 | 28 | /// @private 29 | onOrientationChanged: { 30 | switch (value) { 31 | case this.Horizontal: 32 | this.style("appearance", "slider-horizontal") 33 | this.element.setAttribute('orient', 'horizontal') 34 | break 35 | case this.Vertical: 36 | this.style("appearance", "slider-vertical") 37 | this.element.setAttribute('orient', 'vertical') 38 | break 39 | } 40 | } 41 | 42 | /// returns tag for corresponding element 43 | function getTag() { return 'input' } 44 | 45 | /// gets element native value 46 | function _getValue() { 47 | return this.element.getProperty('value') 48 | } 49 | 50 | /// sets element native value 51 | function _setValue(value) { 52 | this.element.setProperty('value', value) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /input/SearchInput.qml: -------------------------------------------------------------------------------- 1 | /// HTML5 search input item 2 | BaseInput { 3 | property string text; ///< input text string property 4 | width: 100; ///<@private 5 | height: 25; ///<@private 6 | type: "search"; ///<@private 7 | 8 | onTextChanged: { this._updateValue(value) } 9 | 10 | constructor: { 11 | this.element.on("input", function() { 12 | this.text = this._getValue() 13 | }.bind(this)) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /input/TextAreaInput.qml: -------------------------------------------------------------------------------- 1 | BaseInput { 2 | property string text; 3 | width: 150; 4 | height: 100; 5 | 6 | onTextChanged: { 7 | this._updateValue(value) 8 | } 9 | 10 | /// returns tag for corresponding element 11 | function getTag() { return 'textarea' } 12 | 13 | function registerStyle(style, tag) { 14 | style.addRule('textarea', { 15 | "position": "absolute", 16 | "visibility": "inherit", 17 | "border-style": "solid", 18 | "border-width": "0px", 19 | "box-sizing": "border-box", 20 | "resize": "none" 21 | }) 22 | style.addRule('textarea:focus', {"outline": "none"}) 23 | } 24 | 25 | constructor: { 26 | this.element.on("input", function() { 27 | this.text = this._getValue() 28 | }.bind(this)) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /input/TextInput.qml: -------------------------------------------------------------------------------- 1 | ///HTML text input item 2 | BaseInput { 3 | property string text; ///< input text string property 4 | property int maxLength; ///< max available length for the input text 5 | property bool passwordMode: false; ///< show password chars instead of input text flag property 6 | property Color cursorColor: "#fff"; ///< cursor color 7 | height: 20; ///<@private 8 | width: 173; ///<@private 9 | type: passwordMode ? "password" : "text"; ///<@private 10 | 11 | onTextChanged: { this._updateValue(value) } 12 | 13 | constructor: { 14 | this.element.on("input", function() { 15 | this.text = this._getValue() 16 | }.bind(this)) 17 | } 18 | 19 | onMaxLengthChanged: { this.element.setAttribute('maxlength', value) } 20 | onCursorColorChanged: { this.style('caret-color', value) } 21 | } 22 | -------------------------------------------------------------------------------- /input/TimeInput.qml: -------------------------------------------------------------------------------- 1 | ///time selecting input 2 | BaseInput { 3 | property string max; ///< maximum time value 4 | property string min; ///< minimal time value 5 | property string value; ///< current value 6 | width: 75; 7 | height: 20; 8 | type: "time"; 9 | 10 | onMinChanged: { this.element.setProperty('min', value) } 11 | onMaxChanged: { this.element.setProperty('max', value) } 12 | 13 | constructor: { 14 | this.element.on("change", function() { 15 | this.value = this._getValue() 16 | }.bind(this)) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /mixins/CssMixin.qml: -------------------------------------------------------------------------------- 1 | // customize parent with class selector 2 | BaseMixin { 3 | property string rules; 4 | 5 | ///@private 6 | onCompleted: { 7 | var cn = this.parent.componentName; 8 | if (!_globals[cn] && _globals.html5 !== undefined) { 9 | var style = new _globals.html5.html.Element(this, document.createElement('style')) 10 | style.setHtml(this.rules) 11 | var head = _globals.html5.html.getElement(this._context, 'head') 12 | head.append(style) 13 | _globals[cn] = true; 14 | } 15 | this.parent.element.addClass(cn); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /mixins/DisableHoverByTimeoutMixin.qml: -------------------------------------------------------------------------------- 1 | /// this mixin turns off hover and activeHover by timeout and could be used for SmartTV's with autohiding cursor 2 | HoverClickMixin { 3 | id: hoverMixinProto; 4 | property int timeout: 5000; ///< timeout delay 5 | property int mouseX; 6 | property int mouseY; 7 | signal mouseMove; 8 | 9 | Timer { 10 | id: disableHoverTimer; 11 | interval: parent.timeout; 12 | 13 | onTriggered: { 14 | hoverMixinProto._setHover(false) 15 | } 16 | } 17 | 18 | /// @private 19 | constructor: { 20 | this._bindMove(this.enabled) 21 | } 22 | 23 | /// @private 24 | function _updatePosition(event) { 25 | var parent = this.parent 26 | var x = event.offsetX 27 | var y = event.offsetY 28 | if (x >= 0 && y >= 0 && x < parent.width && y < parent.height) { 29 | this.mouseX = x 30 | this.mouseY = y 31 | this.mouseMove(x, y) 32 | return true 33 | } 34 | else 35 | return false 36 | } 37 | 38 | /// @private 39 | function _bindMove(value) { 40 | if (value && !this._mouseMoveBinder) { 41 | this._mouseMoveBinder = new $core.EventBinder(this.element) 42 | this._mouseMoveBinder.on('mousemove', function(event) { 43 | if (!this._updatePosition(event)) 44 | $core.callMethod(event, 'preventDefault') 45 | }.bind(this)) 46 | } 47 | if (this._mouseMoveBinder) 48 | this._mouseMoveBinder.enable(value) 49 | } 50 | 51 | /// @private 52 | function _setHover(value) { 53 | this.value = value 54 | if (this.activeHoverEnabled) 55 | this.activeHover = value 56 | } 57 | 58 | /// @private 59 | onEnabledChanged: { 60 | this._bindMove(value) 61 | } 62 | 63 | /// @private 64 | onValueChanged: { 65 | if (value) 66 | disableHoverTimer.restart() 67 | } 68 | 69 | /// @private 70 | onMouseMove: { 71 | this._setHover(!this._touchEvent) 72 | disableHoverTimer.restart() 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /mixins/DoubleClickMixin.qml: -------------------------------------------------------------------------------- 1 | /// This mixin provides mouse double click event detecting 2 | BaseMouseMixin { 3 | constructor: { 4 | this._bindClicked(this.enabled) 5 | } 6 | 7 | ///@private 8 | function _bindClicked(value) { 9 | if (value && !this._dblClickedBinder) { 10 | this._dblClickedBinder = new _globals.core.EventBinder(this.element) 11 | this._dblClickedBinder.on('dblclick', $core.createSignalForwarder(this.parent, 'doubleClicked').bind(this)) 12 | } 13 | if (this._dblClickedBinder) 14 | this._dblClickedBinder.enable(value) 15 | } 16 | 17 | ///@private 18 | onEnabledChanged: { this._bindClicked(value) } 19 | } 20 | -------------------------------------------------------------------------------- /mixins/DragMixin.qml: -------------------------------------------------------------------------------- 1 | /// makes parent item dragable 2 | BaseMouseMixin { 3 | property bool pressed; ///< is mouse pressed flag 4 | property int top; ///< top border 5 | property int left; ///< left border 6 | property int right; ///< right border 7 | property int bottom; ///< bottom border 8 | property enum direction { Any, Vertical, Horizontal }; ///< available drag direction 9 | 10 | ///@private 11 | constructor: { 12 | this._bindPressed(this.enabled) 13 | this.moved = $core.createSignalForwarder(this.parent, 'moved') 14 | } 15 | 16 | ///@private 17 | function _moveHandler(e) { 18 | if (e.changedTouches) 19 | e = e.changedTouches[0] 20 | 21 | if (this.direction !== this.Horizontal) { 22 | var eY = e.clientY, sY = this._startY, top = this.top, bottom = this.bottom 23 | if (bottom && (eY - sY > bottom)) { 24 | this.parent.y = bottom 25 | } 26 | else if (top && (eY - sY < top)) 27 | this.parent.y = top 28 | else { 29 | this.parent.y = eY - sY 30 | } 31 | } 32 | if (this.direction !== this.Vertical) { 33 | var eX = e.clientX, sX = this._startX, left = this.left, right = this.right 34 | if (right && (eX - sX > right)) { 35 | this.parent.x = right 36 | } 37 | else if (left && (eX - sX < left)) 38 | this.parent.x = left 39 | else { 40 | this.parent.x = eX - sX 41 | } 42 | } 43 | 44 | if (Math.abs(this._initY - e.clientY) < 4 && Math.abs(this._initX - e.clientX) < 4) 45 | return 46 | 47 | this.moved(e) // emit moved signal to the parent 48 | } 49 | 50 | ///@private 51 | function _downHandler(e) { 52 | this.pressed = true 53 | 54 | if (e.changedTouches) 55 | e = e.changedTouches[0] 56 | 57 | this._startX = e.clientX - this.parent.x 58 | this._startY = e.clientY - this.parent.y 59 | this._initX = e.clientX 60 | this._initY = e.clientY 61 | if (!this._dmMoveBinder) { 62 | this._dmMoveBinder = new _globals.core.EventBinder(context.window || this.element) 63 | 64 | this._dmMoveBinder.on('mousemove', this._moveHandler.bind(this)) 65 | this._dmMoveBinder.on('touchmove', this._moveHandler.bind(this)) 66 | 67 | this._dmMoveBinder.on('mouseup', function() { 68 | this.pressed = false 69 | this._dmMoveBinder.enable(false) 70 | }.bind(this)) 71 | 72 | this._dmMoveBinder.on('touchend', function() { 73 | this.pressed = false 74 | this._dmMoveBinder.enable(false) 75 | }.bind(this)) 76 | } 77 | this._dmMoveBinder.enable(true) 78 | 79 | $core.callMethod(e, 'stopPropagation') 80 | } 81 | 82 | ///@private 83 | function _bindPressed(value) { 84 | if (value && !this._dmPressBinder) { 85 | this._dmPressBinder = new _globals.core.EventBinder(this.element) 86 | this._dmPressBinder.on('mousedown', this._downHandler.bind(this)) 87 | this._dmPressBinder.on('touchstart', this._downHandler.bind(this)) 88 | } 89 | if (this._dmPressBinder) 90 | this._dmPressBinder.enable(value) 91 | } 92 | 93 | ///@private 94 | onEnabledChanged: { 95 | this._bindPressed(value) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /mixins/DropZoneMixin.qml: -------------------------------------------------------------------------------- 1 | /// Object for loading files via drag'n'drop 2 | BaseMixin { 3 | signal filesAdded; ///< any file dropped inside DropZone signal 4 | 5 | ///@private 6 | constructor: { 7 | var parent = this.parent 8 | var element = parent.element 9 | element.style('pointer-events', 'auto') 10 | element.style('touch-action', 'auto') 11 | 12 | element.on('dragover', function(e) { 13 | $core.callMethod(e, 'stopPropagation') 14 | $core.callMethod(e, 'preventDefault') 15 | e.dataTransfer.dropEffect = 'copy'; 16 | }) 17 | 18 | var self = this 19 | element.on('drop', function(e) { 20 | $core.callMethod(e, 'stopPropagation') 21 | $core.callMethod(e, 'preventDefault') 22 | self.filesAdded(e.dataTransfer.files) 23 | }.bind(parent)); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /mixins/FocusOnHoverMixin.qml: -------------------------------------------------------------------------------- 1 | HoverMixin { 2 | cursor: "pointer"; 3 | 4 | onValueChanged: { 5 | if (value) 6 | this.parent.setFocus() 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /mixins/HoverClickMixin.qml: -------------------------------------------------------------------------------- 1 | /// this mixin provides mouse hover and click events handling 2 | BaseMouseMixin { 3 | property bool clickable: true; 4 | property bool activeHoverEnabled: false; 5 | property bool value; 6 | property bool activeHover: false; 7 | 8 | constructor: { 9 | this._bindClick(this.clickable) 10 | this._bindHover(this.enabled) 11 | this._bindActiveHover(this.activeHoverEnabled) 12 | } 13 | 14 | ///@private 15 | function _bindClick(value) { 16 | if (value && !this._hmClickBinder) { 17 | this._hmClickBinder = new _globals.core.EventBinder(this.element) 18 | this._hmClickBinder.on('click', _globals.core.createSignalForwarder(this.parent, 'clicked').bind(this)) 19 | } 20 | if (this._hmClickBinder) 21 | this._hmClickBinder.enable(value) 22 | } 23 | 24 | ///@private 25 | function _bindHover(value) { 26 | if (value && !this._hmHoverBinder) { 27 | this._hmHoverBinder = new _globals.core.EventBinder(this.parent.element) 28 | if (this._context.backend.capabilities.mouseEnterLeaveSupported) { 29 | this._hmHoverBinder.on('mouseenter', function() { this.value = this._trueUnlessTouchEvent() }.bind(this)) 30 | this._hmHoverBinder.on('mouseleave', function() { this.value = false }.bind(this)) 31 | } else { 32 | this._hmHoverBinder.on('mouseover', function() { this.value = this._trueUnlessTouchEvent() }.bind(this)) 33 | this._hmHoverBinder.on('mouseout', function() { this.value = false }.bind(this)) 34 | } 35 | this._hmHoverBinder.on('touchstart', this._setTouchEvent.bind(this)) 36 | this._hmHoverBinder.on('mouseup', this._resetTouchEvent.bind(this)) 37 | } 38 | if (this._hmHoverBinder) 39 | this._hmHoverBinder.enable(value) 40 | } 41 | 42 | ///@private 43 | function _bindActiveHover(value) { 44 | if (value && !this._hmActiveHoverBinder) { 45 | this._hmActiveHoverBinder = new _globals.core.EventBinder(this.parent.element) 46 | this._hmActiveHoverBinder.on('mouseover', function() { this.activeHover = this._trueUnlessTouchEvent() }.bind(this)) 47 | this._hmActiveHoverBinder.on('mouseout', function() { this.activeHover = false }.bind(this)) 48 | this._hmActiveHoverBinder.on('touchstart', this._setTouchEvent.bind(this)) 49 | this._hmActiveHoverBinder.on('mouseup', this._resetTouchEvent.bind(this)) 50 | } 51 | if (this._hmActiveHoverBinder) 52 | { 53 | this._hmActiveHoverBinder.enable(value) 54 | } 55 | } 56 | 57 | onEnabledChanged: { this._bindHover(value) } 58 | onClickableChanged: { this._bindClick(value) } 59 | onActiveHoverEnabledChanged: { this._bindActiveHover(value) } 60 | } 61 | -------------------------------------------------------------------------------- /mixins/ImageMixin.qml: -------------------------------------------------------------------------------- 1 | /// is the same thing as usual Image but fill it's parent instead of placing it inside 2 | Image { 3 | ///@private 4 | function _createElement(tag) { 5 | this.element = this.parent.element; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /mixins/MouseMixin.qml: -------------------------------------------------------------------------------- 1 | /// Provide mouse signals: clicked, mouseMove, wheel. And properties: mouseX, mouseY, hover, pressed 2 | BaseMouseMixin { 3 | property bool clickable: true; ///< enable mouse click event handling flag 4 | property bool hoverable: true; ///< hoverEnabled property alias 5 | property bool pressable: true; ///< enable mouse click event handling flag 6 | property bool wheelEnabled: true; ///< enable mouse click event handling flag 7 | 8 | ///@private 9 | constructor: { 10 | var parent = this.parent 11 | this._bindClick(this.clickable) 12 | this._bindHover(this.hoverable) 13 | this._bindPressable(this.pressable) 14 | this._bindWheel(this.wheelEnabled) 15 | 16 | if (!exports.hasProperty(parent, "mouseX")) 17 | exports.addProperty(parent, "real", "mouseX", 0.0) 18 | if (!exports.hasProperty(parent, "mouseY")) 19 | exports.addProperty(parent, "real", "mouseY", 0.0) 20 | if (!exports.hasProperty(parent, "hover")) 21 | exports.addProperty(parent, "bool", "hover", false) 22 | if (!exports.hasProperty(parent, "pressed")) 23 | exports.addProperty(parent, "bool", "pressed", false) 24 | } 25 | 26 | ///@private 27 | function _bindClick(value) { 28 | if (value && !this._cmClickBinder) { 29 | this._cmClickBinder = new $core.EventBinder(this.element) 30 | this._cmClickBinder.on('click', $core.createSignalForwarder(this.parent, 'clicked').bind(this)) 31 | } 32 | if (this._cmClickBinder) 33 | this._cmClickBinder.enable(value) 34 | } 35 | 36 | /// @private 37 | function _bindWheel(value) { 38 | if (value && !this._wheelBinder) { 39 | this._clickBinder = new $core.EventBinder(this.element) 40 | this._cmClickBinder.on('mousewheel', $core.createSignalForwarder(this.parent, 'wheel').bind(this)) 41 | } 42 | if (this._clickBinder) 43 | this._clickBinder.enable(value) 44 | } 45 | 46 | ///@private 47 | function _bindHover(value) { 48 | var parent = this.parent 49 | if (value && !this._hoverBinder) { 50 | this._hoverBinder = new $core.EventBinder(this.element) 51 | this._hoverBinder.on('mousemove', function(event) { if (this.updatePosition(event)) $core.callMethod(event, 'preventDefault') }.bind(this)) 52 | this._hoverBinder.on('mouseenter', function() { parent.hover = true }.bind(this)) 53 | this._hoverBinder.on('mouseleave', function() { parent.hover = false }.bind(this)) 54 | } 55 | if (this._hoverBinder) 56 | this._hoverBinder.enable(value) 57 | 58 | if (value && !this._mouseMovebinder) { 59 | this._mouseMovebinder = new $core.EventBinder(this.element) 60 | this._mouseMovebinder.on('mousemove', $core.createSignalForwarder(parent, 'mouseMove').bind(this)) 61 | } 62 | 63 | if (this._mouseMovebinder) 64 | this._mouseMovebinder.enable(value) 65 | } 66 | 67 | ///@private 68 | function _bindPressable(value) { 69 | if (value && !this._pressableBinder) { 70 | this._pressableBinder = new $core.EventBinder(this.element) 71 | var parent = this.parent 72 | this._pressableBinder.on('mousedown', function() { parent.pressed = true }.bind(this)) 73 | this._pressableBinder.on('mouseup', function() { parent.pressed = false }.bind(this)) 74 | } 75 | if (this._pressableBinder) 76 | this._pressableBinder.enable(value) 77 | } 78 | 79 | updatePosition(event): { 80 | var parent = this.parent 81 | if (!parent.recursiveVisible) 82 | return false 83 | 84 | var x = event.offsetX 85 | var y = event.offsetY 86 | 87 | if (x >= 0 && y >= 0 && x < parent.width && y < parent.height) { 88 | parent.mouseX = x 89 | parent.mouseY = y 90 | return true 91 | } else { 92 | return false 93 | } 94 | } 95 | 96 | onHoverableChanged: { this._bindHover(value) } 97 | onClickableChanged: { this._bindClick(value) } 98 | onPressableChanged: { this._bindPressable(value) } 99 | } 100 | -------------------------------------------------------------------------------- /mixins/OrientationMixin.qml: -------------------------------------------------------------------------------- 1 | /// The mixin provides information from the physical orientation of the device. 2 | BaseMixin { 3 | property real alpha; ///< The rotation of the device around the Z axis; that is, the number of degrees by which the device is being twisted around the center of the screen. 4 | property real beta; ///< The rotation of the device around the X axis; that is, the number of degrees, ranged between -180 and 180, by which the device is tipped forward or backward. 5 | property real gamma; ///< The rotation of the device around the Y axis; that is, the number of degrees, ranged between -90 and 90, by which the device is turned left or right. 6 | property bool absolute; ///< Indicates whether or not the device is providing orientation data absolutely (that is, in reference to the Earth's coordinate frame) or using some arbitrary frame determined by the device. 7 | 8 | constructor: { 9 | this._bindOrientation(this.enabled) 10 | } 11 | 12 | /// @private 13 | function _bindOrientation(value) { 14 | if (value && !this._orientationBinder && this._context.window) { 15 | var self = this 16 | this._context.window.on("deviceorientation", function(e) { 17 | self.absolute = e.absolute 18 | self.alpha = e.alpha 19 | self.beta = e.beta 20 | self.gamma = e.gamma 21 | }.bind(this)) 22 | } 23 | if (this._orientationBinder) 24 | this._orientationBinder.enable(value) 25 | } 26 | 27 | ///@private 28 | onEnabledChanged: { this._bindOrientation(value) } 29 | } 30 | -------------------------------------------------------------------------------- /mixins/OverflowMixin.qml: -------------------------------------------------------------------------------- 1 | ///setup parents overflow mode 2 | BaseMixin { 3 | property enum value { Visible, Hidden, Scroll, ScrollX, ScrollY }; ///< overflow mode value, can be: Visible, Hidden, Scroll, ScrollX, ScrollY 4 | 5 | /// @internal 6 | _updateOverflow(value): { 7 | switch(value) { 8 | case this.Visible: 9 | this.parent.style('overflow', 'visible'); 10 | break; 11 | case this.Hidden: 12 | this.parent.style('overflow', 'hidden'); 13 | break; 14 | case this.Scroll: 15 | this.parent.style('overflow', 'auto'); 16 | break; 17 | case this.ScrollX: 18 | this.parent.style('overflow', 'auto'); 19 | this.parent.style('overflow-y', 'hidden'); 20 | break; 21 | case this.ScrollY: 22 | this.parent.style('overflow', 'auto'); 23 | this.parent.style('overflow-x', 'hidden'); 24 | break; 25 | } 26 | } 27 | onValueChanged: { this._updateOverflow(value) } 28 | onCompleted: { this._updateOverflow(this.value) } 29 | } 30 | -------------------------------------------------------------------------------- /mixins/PositionMixin.qml: -------------------------------------------------------------------------------- 1 | ///setup position mode of its parent 2 | BaseMixin { 3 | property enum value { Absolute, Fixed, Relative, Static }; ///< position mode value can be: Absolute, Fixed, Relative, Static 4 | 5 | onValueChanged (name, value): { 6 | switch (value) { 7 | case this.Absolute: 8 | this.parent.style('position', 'absolute'); 9 | break; 10 | case this.Fixed: 11 | this.parent.style('position', 'fixed'); 12 | break; 13 | case this.Relative: 14 | this.parent.style('position', 'relative'); 15 | break; 16 | case this.Static: 17 | this.parent.style('position', 'static'); 18 | break; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /mixins/RightClickMixin.qml: -------------------------------------------------------------------------------- 1 | BaseMouseMixin { 2 | constructor: { 3 | this._bindClicked(this.enabled) 4 | } 5 | 6 | ///@private 7 | function _bindClicked(value) { 8 | if (value && !this._clickedBinder) { 9 | this._clickedBinder = new _globals.core.EventBinder(this.element) 10 | this._clickedBinder.on('mousedown', function(e) { 11 | if (e.which === 3) { 12 | this._emit(e) 13 | } else if (e.button === 2) { 14 | this._emit(e) 15 | } 16 | }.bind(this)) 17 | } 18 | if (this._clickedBinder) 19 | this._clickedBinder.enable(value) 20 | } 21 | 22 | ///@private 23 | function _emit(e) { 24 | this.parent.emitWithArgs('rightClicked', arguments) 25 | $core.callMethod(e, 'stopPropagation') 26 | } 27 | 28 | ///@private 29 | onEnabledChanged: { 30 | this._bindClicked(value) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /mixins/SwipeMixin.qml: -------------------------------------------------------------------------------- 1 | /// mixin provides touch swipe events 2 | BaseMixin { 3 | signal touchMove; ///< @private 4 | signal touchStart; ///< @private 5 | signal touchEnd; ///< @private 6 | signal verticalSwiped; ///< emitted on vertical swipe 7 | signal horizontalSwiped; ///< emitted on horizontal swipe 8 | 9 | constructor: { this._bindTouch(this.enabled) } 10 | 11 | onEnabledChanged: { this._bindTouch(value) } 12 | 13 | ///@private 14 | function _bindTouch(value) { 15 | if (value && !this._touchBinder) { 16 | this._touchBinder = new _globals.core.EventBinder(this.parent.element) 17 | this._touchBinder.on('touchstart', function(event) { this.touchStart(event) }.bind(this)) 18 | this._touchBinder.on('touchend', function(event) { this.touchEnd(event) }.bind(this)) 19 | this._touchBinder.on('touchmove', function(event) { this.touchMove(event) }.bind(this)) 20 | } 21 | if (this._touchBinder) 22 | this._touchBinder.enable(value) 23 | } 24 | 25 | ///@private 26 | onTouchStart(event): { 27 | var box = this.parent.toScreen() 28 | var e = event.touches[0] 29 | var x = e.pageX - box[0] 30 | var y = e.pageY - box[1] 31 | this._startX = x 32 | this._startY = y 33 | this._orientation = null; 34 | this._startTarget = event.target; 35 | } 36 | 37 | ///@private 38 | onTouchMove(event): { 39 | var box = this.parent.toScreen() 40 | 41 | var e = event.touches[0] 42 | var x = e.pageX - box[0] 43 | var y = e.pageY - box[1] 44 | var dx = x - this._startX 45 | var dy = y - this._startY 46 | var adx = Math.abs(dx) 47 | var ady = Math.abs(dy) 48 | var motion = adx > 5 || ady > 5 49 | if (!motion) 50 | return 51 | 52 | if (!this._orientation) 53 | this._orientation = adx > ady ? 'horizontal' : 'vertical' 54 | 55 | // for delegated events, the target may change over time 56 | // this ensures we notify the right target and simulates the mouseleave behavior 57 | while (event.target && event.target !== this._startTarget) 58 | event.target = event.target.parentNode; 59 | if (event.target !== this._startTarget) { 60 | event.target = this._startTarget; 61 | return; 62 | } 63 | 64 | if (this._orientation == 'horizontal') 65 | this.horizontalSwiped(event) 66 | else 67 | this.verticalSwiped(event) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /mixins/TextMixin.qml: -------------------------------------------------------------------------------- 1 | ///mixin which instantiate text in component without creating extra entities 2 | Text { 3 | width: 100%; 4 | height: 100%; 5 | verticalAlignment: Text.AlignVCenter; 6 | horizontalAlignment: Text.AlignHCenter; 7 | 8 | prototypeConstructor: { TextMixinPrototype.defaultProperty = 'text' } 9 | 10 | function _createElement(tag) { 11 | this.element = this.parent.element; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /mixins/TitleMixin.qml: -------------------------------------------------------------------------------- 1 | BaseMixin { 2 | property string value; 3 | 4 | onValueChanged: { this.parent.element.setAttribute('title', value); } 5 | } 6 | -------------------------------------------------------------------------------- /mixins/UserSelectMixin.qml: -------------------------------------------------------------------------------- 1 | ///This object controls how the text is allowed to be selected 2 | BaseMixin { 3 | property enum value { None, Text, All }; ///< text selection mode enumeration 4 | 5 | ///@private 6 | _updateValue: { 7 | var userSelectValue 8 | switch(this.value) { 9 | case this.None: userSelectValue = "none"; break 10 | case this.Text: userSelectValue = "text"; break 11 | case this.All: userSelectValue = "All"; break 12 | } 13 | this.parent.style(this._prefixedName, userSelectValue) 14 | } 15 | 16 | onValueChanged: { this._updateValue() } 17 | 18 | constructor: { 19 | this._prefixedName = (typeof window !== 'undefined')? window.Modernizr.prefixedCSS('user-select'): 'user-select' 20 | this._updateValue() 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pureqml-controls", 3 | "version": "1.2.0", 4 | "description": "Library of the most commonly used QML controls for PureQML qml transpiler. Canvas, YouTube, Google Charts, inputs (native and non-native), mixins, SVG, vk.com, and Yandex components.", 5 | 6 | "repository": { 7 | "type": "git", 8 | "url": "git://github.com/pureqml/controls" 9 | }, 10 | 11 | "bugs": { 12 | "url" : "https://github.com/pureqml/controls/issues" 13 | }, 14 | 15 | "author": "PureQML team", 16 | "license": "MIT", 17 | 18 | "dependencies": { 19 | "pureqml-core": "*" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /pure/Checkbox.qml: -------------------------------------------------------------------------------- 1 | /// simple checkbox control with swipable button 2 | MouseArea { 3 | id: checkboxProto; 4 | property bool checked; ///< is checkbox checked or not 5 | property color panelColor: "#ccc"; ///< panel color 6 | property color checkedColor: "#8BC34A"; ///< button color if checked 7 | property color uncheckedColor: "#F44336"; ///< button color if not checked 8 | height: 40; ///< @private 9 | width: 80; ///< @private 10 | 11 | Rectangle { 12 | anchors.fill: parent; 13 | anchors.topMargin: parent.height / 8; 14 | anchors.bottomMargin: parent.height / 8; 15 | radius: height / 2; 16 | color: parent.panelColor; 17 | } 18 | 19 | Rectangle { 20 | id: checkboxSwitcher; 21 | x: !parent.checked ? 0 : parent.width - width; 22 | height: parent.height; 23 | width: height; 24 | radius: width / 2; 25 | color: parent.checked ? parent.checkedColor : parent.uncheckedColor; 26 | effects.shadow.x: checkboxProto.checked ? -1 : 1; 27 | effects.shadow.color: "#9e9e9e"; 28 | 29 | Behavior on x { Animation { duration: 150; } } 30 | Behavior on color { ColorAnimation { duration: 150; } } 31 | } 32 | 33 | Timer { 34 | id: checkboxDelay; 35 | interval: 100; 36 | 37 | onTriggered: { checkboxProto.checked = checkboxProto._dx && checkboxProto._dx >= 0 } 38 | } 39 | 40 | ///@private 41 | onHorizontalSwiped(event): { 42 | var box = this.toScreen() 43 | var touch = event.touches[0] 44 | var x = touch.pageX - box[0] 45 | this._dx = x - this._startX 46 | checkboxDelay.restart() 47 | } 48 | 49 | ///@private 50 | onMouseMove: { 51 | if (this.pressed) { 52 | this._dx = this.mouseX - this.width / 2 53 | checkboxDelay.restart() 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /pure/FloatingText.qml: -------------------------------------------------------------------------------- 1 | Item { 2 | id: floatingTextProto; 3 | property string text; 4 | property int pixelSize: 21; 5 | property bool running; 6 | property Color color: "#fff"; 7 | height: 24; 8 | clip: true; 9 | 10 | Text { 11 | id: floatingText; 12 | property int delta: parent.width - width; 13 | text: parent.text; 14 | color: parent.color; 15 | font.pixelSize: parent.pixelSize; 16 | 17 | onPaintedWidthChanged: { 18 | if (this.parent.running) 19 | this.parent.startFloatingText() 20 | } 21 | 22 | Behavior on transform { 23 | Animation { 24 | id: floatingAnimation; 25 | duration: -floatingText.delta * 30; 26 | easing: "linear"; 27 | delay: 300; 28 | } 29 | } 30 | } 31 | 32 | Timer { 33 | id: floatingTextTimer; 34 | interval: -floatingText.delta * 30 + 2000; 35 | running: parent.running; 36 | repeat: parent.running; 37 | 38 | onTriggered: { 39 | this.parent.startFloatingText() 40 | floatingText.transform.translateX = floatingText.delta 41 | } 42 | } 43 | 44 | resetPosition: { 45 | floatingAnimation.disable() 46 | floatingText.transform.translateX = 0 47 | floatingAnimation.enable() 48 | } 49 | 50 | startFloatingText: { 51 | this.resetPosition() 52 | if (floatingText.paintedWidth) 53 | floatingText.transform.translateX = floatingText.delta 54 | } 55 | 56 | onRunningChanged: { 57 | if (value) 58 | this.startFloatingText() 59 | else 60 | this.resetPosition() 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /pure/MultiLineText.qml: -------------------------------------------------------------------------------- 1 | Text { 2 | property int linesCount: 2; 3 | wrapMode: Text.WordWrap; 4 | height: Math.min(paintedHeight, font.pixelSize * font.lineHeight * linesCount); 5 | clip: true; 6 | 7 | onLinesCountChanged: { 8 | this.style('line-clamp', this.linesCount); 9 | this.style('-webkit-line-clamp', this.linesCount); 10 | } 11 | 12 | onCompleted: { 13 | this.style("display", "block"); 14 | this.style("display", "-webkit-box"); 15 | this.style("text-overflow", "ellipsis"); 16 | this.style('box-orient', "vertical"); 17 | this.style('-webkit-box-orient', "vertical"); 18 | this.style('line-clamp', this.linesCount); 19 | this.style('-webkit-line-clamp', this.linesCount); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /pure/ProgressBar.qml: -------------------------------------------------------------------------------- 1 | Rectangle { 2 | id: progressBarItem; 3 | property real progress: 0.0; 4 | height: 6; 5 | width: 100; 6 | color: "#707070"; 7 | 8 | Rectangle { 9 | anchors.left: parent.left; 10 | anchors.top: parent.top; 11 | height: parent.height; 12 | width: parent.width * progressBarItem.progress; 13 | gradient: Gradient { 14 | GradientStop { color: "#f2c300"; position: 0.0; } 15 | GradientStop { color: "#e89500"; position: 1.0; } 16 | } 17 | 18 | Behavior on width { Animation { duration: 300; } } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /pure/ScrollBar.qml: -------------------------------------------------------------------------------- 1 | Rectangle { 2 | id: scrollBarProto; 3 | property int contentY; 4 | property int minHeight: 10; 5 | property int contentHeight: 1; 6 | property int displayHeight: 1; 7 | property int displaySeconds: 1000; 8 | property int animationDuration: 300; 9 | property int realContentY: contentHeight ? -contentY * 1.0 / contentHeight * displayHeight : 0; 10 | property int realHeight: contentHeight ? displayHeight * 1.0 / contentHeight * displayHeight : 0; 11 | property int maxContentY: displayHeight - movingBar.height; 12 | property int shift: realContentY > maxContentY ? maxContentY : realContentY; 13 | property bool display: false; 14 | property Color barColor: "#fff"; 15 | width: 6; 16 | height: displayHeight; 17 | opacity: display ? 1.0 : 0.0; 18 | visible: contentHeight > displayHeight; 19 | radius: width / 2; 20 | color: "gray"; 21 | 22 | Rectangle { 23 | id: movingBar; 24 | width: 100%; 25 | transform.translateY: parent.shift; 26 | radius: width / 2; 27 | height: parent.realHeight < parent.minHeight ? parent.minHeight : parent.realHeight; 28 | color: parent.barColor; 29 | 30 | Behavior on transform { Animation { duration: scrollBarProto.animationDuration; easing: "ease-out"; } } 31 | } 32 | 33 | Timer { 34 | id: displayTimer; 35 | interval: parent.displaySeconds; 36 | 37 | onTriggered: { this.parent.display = false } 38 | } 39 | 40 | onHeightChanged, 41 | onContentYChanged: { this.show() } 42 | 43 | show: { 44 | this.display = true 45 | displayTimer.restart() 46 | } 47 | 48 | Behavior on opacity { Animation { duration: scrollBarProto.animationDuration; easing: "ease-out"; } } 49 | } 50 | -------------------------------------------------------------------------------- /pure/Subtitles.qml: -------------------------------------------------------------------------------- 1 | Resource { 2 | property real progress; ///< bind player progress with this property 3 | property string text; ///< current subttile text will be changed on corresponded time 4 | 5 | onProgressChanged: { 6 | var blocks = this._textBlocks 7 | if (!blocks || blocks.length == 0) { 8 | this.text = "" 9 | return 10 | } 11 | 12 | if (value < blocks[0].startTime || value > blocks[blocks.length - 1].endTime) { 13 | this.text = "" 14 | return 15 | } 16 | 17 | var found = false 18 | for (var i in blocks) { 19 | if (value >= blocks[i].startTime && value <= blocks[i].endTime) { 20 | this.text = blocks[i].text 21 | found = true 22 | } 23 | } 24 | 25 | if (!found) 26 | this.text = "" 27 | } 28 | 29 | ///turn subtitles off 30 | turnOff: { 31 | this._textBlocks = null 32 | this.text = "" 33 | this.url = "" 34 | } 35 | 36 | onDataChanged: { 37 | var lines = value.split('\n'); 38 | var textBlocks = [] 39 | var startTime = 0 40 | var endTime = 0 41 | var blockNumber = 0 42 | var text = "" 43 | if (!value) 44 | return 45 | for (var i in lines) { 46 | var line = lines[i].trim() 47 | if (line == "") { 48 | if (text.trim() != "") 49 | textBlocks.push({ text: text, startTime: startTime, endTime: endTime, blockNumber: blockNumber }) 50 | text = "" 51 | startTime = 0 52 | endTime = 0 53 | blockNumber = 0 54 | } else if (this.digitRegExp.test(line)) { 55 | blockNumber = parseInt(line) 56 | } else if (line.indexOf("-->") > 0) { 57 | var timeStrings = line.split("-->") 58 | if (timeStrings.length < 2) 59 | continue 60 | var startTimeStr = timeStrings[0].trim() 61 | var endTimeStr = timeStrings[1].trim() 62 | startTime = this.getTimeDuration(startTimeStr) 63 | endTime = this.getTimeDuration(endTimeStr) 64 | } else { 65 | if (text) 66 | text += "
" 67 | text += line 68 | } 69 | } 70 | 71 | log("got subtitles", textBlocks.length, "lines") 72 | this._textBlocks = textBlocks 73 | } 74 | 75 | ///@private 76 | getTimeDuration(str): { 77 | var parts = str.split(":") 78 | var res = 0 79 | var hours = parseInt(parts[0]) * 3600 80 | var min = parseInt(parts[1]) * 60 81 | var sec = parseInt(parts[2].split(",")[0]) 82 | res = hours + min + sec 83 | return res 84 | } 85 | 86 | constructor: { this.digitRegExp = /^\d+$/; } 87 | } 88 | -------------------------------------------------------------------------------- /pure/TextEdit.qml: -------------------------------------------------------------------------------- 1 | Rectangle { 2 | id: textEditProto; 3 | property lazy paddings: Paddings {} 4 | property string placeholder; 5 | property alias horizontalAlignment: innerText.horizontalAlignment; 6 | property alias backgroundColor: color; 7 | property alias text: innerText.text; 8 | property alias font: innerText.font; 9 | property alias color: innerText.color; 10 | property bool cursorVisible: true; 11 | focus: true; 12 | clip: true; 13 | 14 | Rectangle { 15 | id: cursor; 16 | x: innerText.paintedWidth; 17 | width: 2; 18 | height: innerText.paintedHeight; 19 | anchors.verticalCenter: parent.verticalCenter; 20 | color: innerText.color; 21 | visible: parent.activeFocus && textEditProto.cursorVisible; 22 | } 23 | 24 | Text { 25 | id: innerText; 26 | anchors.verticalCenter: parent.verticalCenter; 27 | } 28 | 29 | Timer { 30 | running: parent.activeFocus && textEditProto.cursorVisible; 31 | repeat: true; 32 | interval: 1000; 33 | 34 | onTriggered: { cursor.visible = !cursor.visible; } 35 | } 36 | 37 | removeChar: { 38 | var text = textEditProto.text 39 | textEditProto.text = text.slice(0, text.length - 1) 40 | } 41 | 42 | onPlaceholderChanged: { this.element.setAttribute('placeholder', value); } 43 | } 44 | -------------------------------------------------------------------------------- /pure/format.js: -------------------------------------------------------------------------------- 1 | exports.currency = function(v, n, x) { 2 | var re = '\\d(?=(\\d{' + (x || 3) + '})+' + (n > 0 ? '\\.' : '$') + ')'; 3 | return v.toFixed(Math.max(0, ~~n)).replace(new RegExp(re, 'g'), '$&,'); 4 | } 5 | 6 | exports.format = function() { 7 | var args = [].slice.call(arguments); 8 | var initial = args.shift(); 9 | 10 | function replacer (text, replacement) { 11 | return text.replace('%s', replacement); 12 | } 13 | return args.reduce(replacer, initial); 14 | } -------------------------------------------------------------------------------- /pure/keyboard/Keyboard.qml: -------------------------------------------------------------------------------- 1 | Item { 2 | id: keyboardProto; 3 | signal keySelected; 4 | signal backspase; 5 | property int currentRow; 6 | width: 420; 7 | height: 480; 8 | 9 | KeyboardModel { id: keyboardModel; } 10 | 11 | ListView { 12 | anchors.fill: parent; 13 | spacing: 5; 14 | model: keyboardModel; 15 | keyNavigationWraps: false; 16 | delegate: Item { 17 | width: parent.width; 18 | height: 45; 19 | 20 | ListView { 21 | anchors.fill: parent; 22 | orientation: ListView.Horizontal; 23 | spacing: 5; 24 | model: KeyboardRowModel { 25 | parentModel: keyboardModel; 26 | begin: model.index * 7; 27 | end: begin + 7; 28 | } 29 | delegate: KeyboardDelegate { } 30 | 31 | onCurrentIndexChanged: { keyboardProto.currentRow = this.currentIndex; } 32 | onLeftPressed: { --this.currentIndex; } 33 | 34 | onRightPressed: { 35 | if (this.currentIndex == this.count - 1) 36 | event.accepted = false; 37 | else 38 | ++this.currentIndex; 39 | } 40 | 41 | onSelectPressed: { 42 | var row = this.model.get(this.currentIndex); 43 | if (row.text) 44 | keyboardProto.keySelected(row.text); 45 | else 46 | keyboardProto.backspase(); 47 | } 48 | 49 | onActiveFocusChanged: { 50 | if (this.activeFocus) 51 | this.currentIndex = keyboardProto.currentRow; 52 | } 53 | } 54 | } 55 | 56 | //TODO: Try something better this hardcode. 57 | onDownPressed: { 58 | if (keyboardModel.mode != KeyboardModel.Special && this.currentIndex == this.count - 3) { 59 | if (keyboardProto.currentRow == 3 || keyboardProto.currentRow == 4) 60 | keyboardProto.currentRow = 3; 61 | if (keyboardProto.currentRow == 5 || keyboardProto.currentRow == 6) 62 | keyboardProto.currentRow = 4; 63 | } else if (keyboardModel.mode != KeyboardModel.Special && this.currentIndex == this.count - 2) { 64 | if (keyboardProto.currentRow == 0 || keyboardProto.currentRow == 1) 65 | keyboardProto.currentRow = 0; 66 | if (keyboardProto.currentRow == 2 || keyboardProto.currentRow == 3) 67 | keyboardProto.currentRow = 1; 68 | if (keyboardProto.currentRow == 4) 69 | keyboardProto.currentRow = 2; 70 | } 71 | this.currentIndex++; 72 | } 73 | 74 | onUpPressed: { 75 | if (keyboardModel.mode != KeyboardModel.Special && this.currentIndex == this.count - 1) { 76 | if (keyboardProto.currentRow == 1) 77 | keyboardProto.currentRow = 3; 78 | if (keyboardProto.currentRow == 2) 79 | keyboardProto.currentRow = 4; 80 | } else if (keyboardModel.mode != KeyboardModel.Special && this.currentIndex == this.count - 2) { 81 | if (keyboardProto.currentRow == 4) 82 | keyboardProto.currentRow = 5; 83 | } 84 | this.currentIndex--; 85 | } 86 | } 87 | 88 | switchLanguage: { keyboardModel.switchLanguage(); } 89 | switchCase: { keyboardModel.switchCase(); } 90 | } 91 | -------------------------------------------------------------------------------- /pure/keyboard/KeyboardDelegate.qml: -------------------------------------------------------------------------------- 1 | Rectangle { 2 | id: key; 3 | height: 45; 4 | width: model.widthScale ? model.widthScale * (height + 5) - 5 : height; 5 | color: model.contextColor ? model.contextColor : "#444"; 6 | border.color: "#fff"; 7 | border.width: activeFocus && parent.activeFocus ? 5 : 0; 8 | 9 | Text { 10 | id: keyText; 11 | anchors.centerIn: parent; 12 | text: model.text; 13 | color: "#fff"; 14 | } 15 | 16 | Image { 17 | anchors.centerIn: parent; 18 | source: model.icon; 19 | visible: model.icon; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /pure/keyboard/KeyboardModel.qml: -------------------------------------------------------------------------------- 1 | ListModel { 2 | id: keyboardModel; 3 | property enum mode { LowerCase, UpperCase, Special }; 4 | property enum language { Eng, Rus }; 5 | /*property string rusLetters: "абвгдеёжзийклмнопрстуфхцчшщъыьэюя.,1234567890";*/ 6 | /*property string engLetters: "abcdefghijklmnopqrstuvwxyz.,1234567890";*/ 7 | /*property string rusLettersUp: "АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ.,1234567890";*/ 8 | /*property string engLettersUp: "ABCDEFGHIJKLMNOPQRSTUVWXYZ.,1234567890";*/ 9 | /*property string special: "!#$%&+?/:;_-*@";*/ 10 | /*property string letters: mode == Special ? special :*/ 11 | /*language == 0 && mode == LowerCase ? rusLetters :*/ 12 | /*language == 0 && mode == UpperCase ? rusLettersUp :*/ 13 | /*language == 1 && mode == LowerCase ? engLetters : engLettersUp;*/ 14 | 15 | onModeChanged: { this.updateLetters() } 16 | onLanguageChanged: { this.updateLetters() } 17 | 18 | fill: { 19 | this.clear(); 20 | this.append({}); 21 | this.append({}); 22 | if (this.mode == this.Special) 23 | return; 24 | this.append({}); 25 | this.append({}); 26 | this.append({}); 27 | this.append({}); 28 | this.append({}); 29 | if (this.language == this.Rus) 30 | this.append({}); 31 | } 32 | 33 | switchLanguage: { 34 | this.language = this.language == this.Rus ? this.Eng : this.Rus 35 | this.fill(); 36 | } 37 | 38 | switchCase: { 39 | this.mode = (++this.mode % 3); 40 | this.fill(); 41 | } 42 | 43 | getCaseIndex: { return this.mode == this.LowerCase ? 0 : 1 } 44 | 45 | updateLetters: { 46 | var mode = this.mode 47 | if (mode == this.Special) 48 | this.letters = this._special 49 | else 50 | this.letters = this.language == this.Rus ? this._rusLetters[this.getCaseIndex()] : this._engLetters[this.getCaseIndex()] 51 | } 52 | 53 | onCompleted: { 54 | this._rusLetters = ["абвгдеёжзийклмнопрстуфхцчшщъыьэюя.,1234567890", "АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ.,1234567890"]; 55 | this._engLetters = ["abcdefghijklmnopqrstuvwxyz.,1234567890", "ABCDEFGHIJKLMNOPQRSTUVWXYZ.,1234567890"]; 56 | this._special = "!#$%&+?/:;_-*@" 57 | this.updateLetters() 58 | this.fill(); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /pure/keyboard/KeyboardRowModel.qml: -------------------------------------------------------------------------------- 1 | ListModel { 2 | property int begin; 3 | property int end; 4 | property Object parentModel; 5 | 6 | onCompleted: { 7 | var letters = this.parentModel.letters; 8 | var last = letters.length > this.end ? this.end : letters.length; 9 | for (var i = this.begin; i < last; ++i) 10 | this.append({ text: letters[i] }); 11 | 12 | if (last == this.end - 4) { 13 | this.append({ icon: "res/backspace.png", contextColor: "#f33", widthScale: 2 }); 14 | this.append({ text: " ", icon: "res/space.png", contextColor: "#33f", widthScale: 2 }); 15 | } else if (this.end > letters.length) { 16 | this.append({ text: "@", widthScale: 2 }); 17 | this.append({ text: ".com", widthScale: 3 }); 18 | this.append({ text: ".ru", widthScale: 2 }); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /svg/ExternalPath.qml: -------------------------------------------------------------------------------- 1 | Path { 2 | function externalTarget() { return true } 3 | } -------------------------------------------------------------------------------- /svg/Line.qml: -------------------------------------------------------------------------------- 1 | SvgBase { 2 | property int x1; 3 | property int x2; 4 | property int y1; 5 | property int y2; 6 | property Color color: "red"; 7 | property int width: 2; 8 | 9 | /// returns tag for corresponding element 10 | function getTag() { return 'line' } 11 | 12 | /// @internal 13 | onColorChanged, 14 | onWidthChanged: { 15 | this.element.setAttribute('style', 'stroke:' + _globals.core.Color.normalize(this.color) + ';stroke-width:' + this.width +';') 16 | } 17 | 18 | onX1Changed: { 19 | this.element.setAttribute('x1', value) 20 | } 21 | onX2Changed: { 22 | this.element.setAttribute('x2', value) 23 | } 24 | onY1Changed: { 25 | this.element.setAttribute('y1', value) 26 | } 27 | onY2Changed: { 28 | this.element.setAttribute('y2', value) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /svg/Path.qml: -------------------------------------------------------------------------------- 1 | SvgBase { 2 | property int x1; 3 | property int x2; 4 | property int y1; 5 | property int y2; 6 | property Color color: "red"; 7 | property string fill: "none"; 8 | property int width: 2; 9 | 10 | /// returns tag for corresponding element 11 | function getTag() { return 'path' } 12 | 13 | onColorChanged: { 14 | this.element.setAttribute('stroke', _globals.core.Color.normalize(value)) 15 | } 16 | 17 | onFillChanged: { 18 | this.element.setAttribute('fill', value) 19 | } 20 | 21 | onWidthChanged: { 22 | this.element.setAttribute('stroke-width', value) 23 | } 24 | 25 | onX1Changed, 26 | onX2Changed, 27 | onY1Changed, 28 | onY2Changed: { 29 | this.element.setAttribute('d', this.buildPath()) 30 | } 31 | 32 | function buildPath() { 33 | var x1 = this.x1, x2 = this.x2, y1 = this.y1, y2 = this.y2 34 | var xb = (x2 - x1) / 2 + x1 35 | 36 | var d = "M" + x1 + "," + y1 + " C" + xb + "," + y1 + " " + xb + "," + y2 + " " + x2 + "," + y2 37 | return d; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /svg/SvgBase.qml: -------------------------------------------------------------------------------- 1 | Object { 2 | property Object target; 3 | 4 | constructor: { 5 | if (this.parent) { 6 | if (this.element) 7 | throw new Error('double ctor call') 8 | 9 | this._createElement(this.getTag(), this.externalTarget()) 10 | } //no parent == top level element, skip 11 | } 12 | 13 | ///@private specialized implementation of element creation in a certain namespace. 14 | function _createElement(tag, append) { 15 | this.element = new _globals.html5.html.Element(this, document.createElementNS('http://www.w3.org/2000/svg', tag)) 16 | if (!append) { 17 | this.parent.element.append(this.element) 18 | } 19 | } 20 | 21 | ///@private returns tag for corresponding element 22 | function getTag() { return 'svg' } 23 | 24 | ///@private 25 | function registerStyle(style, tag) { 26 | style.addRule(tag, { 27 | "position": "absolute", 28 | "visibility": "inherit", 29 | "overflow": "visible" 30 | }) 31 | } 32 | 33 | ///@private 34 | function externalTarget() { return false } 35 | 36 | onTargetChanged: { value.element.remove(); value.element.append(this.element) } 37 | 38 | ///@private 39 | function style(name, style) { 40 | var element = this.element 41 | if (element) 42 | return element.style(name, style) 43 | else 44 | log('WARNING: style skipped:', name, style) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /svg/SvgContainer.qml: -------------------------------------------------------------------------------- 1 | Item { 2 | /// returns tag for corresponding element 3 | function getTag() { return 'svg' } 4 | 5 | /// @private 6 | function registerStyle(style, tag) { 7 | style.addRule(tag, { 8 | 'position': 'absolute', 9 | 'visibility': 'inherit', 10 | 'overflow': 'visible' 11 | }) 12 | } 13 | 14 | /// specialized implementation of element creation in a certain namespace. 15 | function _createElement(tag) { 16 | this._attachElement(new _globals.html5.html.Element(this._context, document.createElementNS('http://www.w3.org/2000/svg', tag))) 17 | } 18 | 19 | onWidthChanged: { 20 | this.style('width', value); 21 | this.element.setAttribute('width', value); 22 | } 23 | 24 | onHeightChanged: { 25 | this.style('height', value - this._topPadding); 26 | this.element.setAttribute('height', value); 27 | } 28 | 29 | onYChanged: { this.style('top', value); } 30 | onXChanged: { this.style('left', value); } 31 | onClipChanged: { this.style('overflow', value? 'hidden': 'visible'); } 32 | } 33 | -------------------------------------------------------------------------------- /web/Box.qml: -------------------------------------------------------------------------------- 1 | Object { 2 | property string top; 3 | property string left; 4 | property string right; 5 | property string bottom; 6 | property string prefix; // dynamic update not supported! 7 | 8 | onTopChanged: { this.parent.style(this.prefix + 'top', value); } 9 | onLeftChanged: { this.parent.style(this.prefix + 'left', value); } 10 | onRightChanged: { this.parent.style(this.prefix + 'right', value); } 11 | onBottomChanged: { this.parent.style(this.prefix + 'bottom', value); } 12 | } 13 | -------------------------------------------------------------------------------- /web/Button.qml: -------------------------------------------------------------------------------- 1 | /// HTML button controls 2 | Rectangle { 3 | signal clicked; ///< button clicked signal 4 | property string text; ///< button inner text 5 | property Font font: Font { } ///< button texts font 6 | property lazy paddings: Paddings {} ///< inner text paddings 7 | property HoverMixin hover: HoverMixin { cursor: "pointer"; } 8 | property color textColor; 9 | property int paintedWidth; 10 | property int paintedHeight; 11 | property bool enabled: true; 12 | width: paintedWidth; 13 | height: paintedHeight; 14 | 15 | ///@private 16 | onTextChanged: { this.element.setHtml(value, this); this._updateSize(); } 17 | 18 | ///@private 19 | onWidthChanged: { this.element.style("width", value); } 20 | 21 | ///@private 22 | onHeightChanged: { this.element.style("height", value ); } 23 | 24 | ///@private 25 | onTextColorChanged: { this.element.style('color', _globals.core.Color.normalize(value)); } 26 | 27 | onEnabledChanged: { 28 | if (value) 29 | this.element.removeAttribute('disabled') 30 | else 31 | this.element.setAttribute('disabled', '') 32 | } 33 | 34 | ///@private returns tag for corresponding element 35 | function getTag() { return 'button' } 36 | 37 | ///@private 38 | function registerStyle(style, tag) { 39 | style.addRule(tag, { 40 | "position": "absolute", 41 | "visibility": "inherit", 42 | "text-decoration": "none", 43 | "border": "none", 44 | "outline": "none", 45 | "box-sizing": "content-box", 46 | "padding": 0 47 | }) 48 | } 49 | 50 | ///@private 51 | function _updateSize() { 52 | this._context.delayedAction('button:update-size', this, this._updateSizeImpl) 53 | } 54 | 55 | ///@private 56 | function _updateSizeImpl() { 57 | this.element.style({ width: 'auto', height: 'auto'}) //no need to reset it to width, it's already there 58 | this.element.updateStyle() 59 | 60 | this.paintedWidth = this.element.fullWidth() 61 | this.paintedHeight = this.element.fullHeight() 62 | 63 | this.element.style({ width: this.width, height: this.height }) 64 | } 65 | 66 | ///@private 67 | constructor: { 68 | var self = this 69 | this.element.on('click', function() { self.clicked() }) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /web/Canvas.qml: -------------------------------------------------------------------------------- 1 | Item { 2 | property bool smooth: true; ///< if false, image will be pixelated 3 | 4 | function getTag() { return 'canvas' } 5 | function registerStyle(style, tag) { 6 | style.addRule(tag, { 7 | "position": "absolute", 8 | "visibility": "inherit" 9 | }) 10 | } 11 | function getContext(name) { return this.element.dom.getContext(name) } 12 | 13 | onSmoothChanged: { 14 | this.style('image-rendering', value? 'auto': 'pixelated') 15 | } 16 | 17 | onWidthChanged: { this.element.dom.width = value; } 18 | onHeightChanged: { this.element.dom.height = value; } 19 | } 20 | -------------------------------------------------------------------------------- /web/CodeHighlighter.qml: -------------------------------------------------------------------------------- 1 | ///item for displaying code with syntax highlighting powered by highlight.js 2 | Item { 3 | property int contentWidth; ///< content width 4 | property int contentHeight; ///< content height 5 | property string code; ///< code string 6 | property string language; ///< programming language 7 | property Font font: Font {} ///< code text font 8 | property int tabWidth: 2; ///< replace tabs with spaces (-1 to turn off) 9 | height: contentHeight; 10 | width: contentWidth; 11 | 12 | /// @private 13 | onWidthChanged, onHeightChanged: { this._updateSize(); } 14 | 15 | /// @private 16 | function _highlightBlock() { 17 | if (this.tabWidth >= 0) { 18 | //FIXME: this is global - find a way to reconfigure each block or create shared configuration 19 | var tab = new Array(this.tabWidth + 1).join(' ') 20 | window.hljs.configure({tabReplace: tab}) 21 | } 22 | window.hljs.highlightBlock(this._code.dom) 23 | } 24 | 25 | /// @private 26 | onCodeChanged: { 27 | this._code.dom.innerHTML = value 28 | this._highlightBlock() 29 | this._updateSize() 30 | } 31 | 32 | /// @private 33 | onLanguageChanged: { 34 | this._code.dom.className = value 35 | this._highlightBlock() 36 | } 37 | 38 | /// @private returns tag for corresponding element 39 | function getTag() { return 'pre' } 40 | 41 | /// @private 42 | function registerStyle(style, tag) { 43 | style.addRule(tag, { 44 | 'position': 'absolute', 45 | 'visibility': 'inherit', 46 | 'margin': '0px', 47 | 'pointer-events': 'auto', 48 | 'touch-action': 'auto' 49 | }) 50 | } 51 | 52 | /// @private 53 | constructor: { 54 | if (!window.hljs) { 55 | log("hljs is not defined! Maybe you forget to attach highlight.js file.") 56 | return 57 | } 58 | this._code = this._context.createElement('code') 59 | this._code.dom.style.width = "auto" 60 | this._code.dom.style.height = "auto" 61 | this._code.dom.style.font = "inherit" 62 | this.element.append(this._code) 63 | } 64 | 65 | /// @private 66 | function _updateSize() { 67 | this.contentWidth = this._code.dom.scrollWidth 68 | this.contentHeight = this._code.dom.scrollHeight 69 | if (this.height && this.contentHeight != this.height) 70 | this._code.dom.style.height = "inherit" 71 | if (this.width && this.contentWidth != this.width) 72 | this._code.dom.style.width = "inherit" 73 | var style = { width: this.width, height: this.height } 74 | this.style(style) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /web/DataList.qml: -------------------------------------------------------------------------------- 1 | ElementWithModel { 2 | property string domId; 3 | property string valueProperty: "value"; 4 | 5 | constructor: { 6 | var element = this.element 7 | var id = "datalist-" + element._uniqueId 8 | element.setAttribute("id", id) 9 | this.domId = id 10 | } 11 | 12 | function getTag() { 13 | return "datalist" 14 | } 15 | 16 | /// @private 17 | function _createValue(row) { 18 | var value = row[this.valueProperty] 19 | if (this.trace) 20 | log("DataList::createValue", value) 21 | 22 | var el = document.createElement("option"); 23 | el.setAttribute("value", value) 24 | return el 25 | } 26 | 27 | /// @private 28 | function _updateValue(el, row) { 29 | var value = row[this.valueProperty] 30 | if (this.trace) 31 | log("DataList::updateValue", value) 32 | 33 | el.setAttribute("value", value) 34 | return el 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /web/DropDown.qml: -------------------------------------------------------------------------------- 1 | ///html drop down list chooser 2 | ElementWithModel { 3 | property Font font: Font {} 4 | property Color color: "#000"; 5 | property int currentIndex; ///< current option index 6 | property int count; ///< drop down options count 7 | property string value; ///< current option value 8 | property string text; ///< current option text 9 | property bool disabled: false; ///< set true to disable changing selection 10 | property string valueProperty: "value"; 11 | property string textProperty: "text"; 12 | width: 100; ///<@private 13 | height: 40; ///<@private 14 | 15 | constructor: { 16 | this.count = 0 17 | this.element.style('pointer-events', 'auto') 18 | this.element.style('touch-action', 'auto') 19 | this.element.on("change", function() { 20 | this.value = this.element.dom.value 21 | var idx = this.element.dom.selectedIndex 22 | this.currentIndex = idx 23 | this.text = this.element.dom[idx].label 24 | }.bind(this)) 25 | this.element.style('pointer-events', 'auto') 26 | this.element.style('touch-action', 'auto') 27 | } 28 | 29 | onDisabledChanged: { 30 | this.element.dom.disabled = this.disabled; 31 | } 32 | 33 | /// @private 34 | onWidthChanged, onHeightChanged: { this._updateSize(); } 35 | 36 | /// @private 37 | function getTag() { return 'select' } 38 | 39 | /// @private 40 | function registerStyle(style, tag) { 41 | style.addRule(tag, { 42 | "position": "absolute", 43 | "visibility": "inherit", 44 | "margin": "0px" 45 | }) 46 | } 47 | 48 | /// @private 49 | function _updateSize() { 50 | var style = { width: this.width, height: this.height } 51 | this.style(style) 52 | } 53 | 54 | /** 55 | * add option into select 56 | * @param {string} value - new option value 57 | * @param {string} text - new option text 58 | */ 59 | append(value, text): { 60 | this.element.append(this._createOption(value, text)) 61 | ++this.count 62 | } 63 | 64 | ///remove all options from dropdown list 65 | clear: { 66 | if (!this.element.dom.options || !this.element.dom.options.length) 67 | return 68 | 69 | var options = this.element.dom.options 70 | for (var i = options.length - 1; i >= 0; --i) 71 | options[i].remove() 72 | this.count = 0; 73 | } 74 | 75 | /** 76 | * remove option from drop down list by index 77 | * @param {number} idx - new option value 78 | */ 79 | remove(idx): { 80 | if (!this.element.dom.options || idx >= this.element.dom.options.length || idx < 0) { 81 | log("bad index") 82 | return 83 | } 84 | this.element.dom.options[idx].remove() 85 | --this.count 86 | } 87 | 88 | onCurrentIndexChanged: { 89 | this.element.dom.value = this.element.dom.options[value].value 90 | this.value = this.element.dom.value; 91 | } 92 | 93 | onCountChanged: { 94 | if (value == 1) { 95 | this.value = this.element.dom.value 96 | this.text = this.element.dom[0].label 97 | this.currentIndex = 0 98 | } 99 | } 100 | 101 | /// @private 102 | function _createOption(value, text) { 103 | var option = this._context.createElement('option') 104 | option.setAttribute('value', value) 105 | option.setHtml(text) 106 | return option 107 | } 108 | 109 | /// @private 110 | function _createValue(row) { 111 | var value = row[this.valueProperty] 112 | var text = row[this.textProperty] 113 | 114 | if (this.trace) 115 | log("DataList::createValue", value) 116 | 117 | var el = this._createOption(value, text) 118 | return el.dom 119 | } 120 | 121 | /// @private 122 | function _updateValue(el, row) { 123 | var value = row[this.valueProperty] 124 | var text = row[this.textProperty] 125 | 126 | if (this.trace) 127 | log("DataList::updateValue", value) 128 | 129 | el.setAttribute("value", value) 130 | el.innerHTML = text 131 | return el 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /web/ElementWithModel.qml: -------------------------------------------------------------------------------- 1 | Item { 2 | property Model model; 3 | property bool trace; 4 | 5 | onModelChanged: { 6 | if (this._modelAttached) { 7 | this._modelAttached.detachFrom(this) 8 | } 9 | this.model.attachTo(this) 10 | } 11 | 12 | /// @private 13 | function _onReset() { 14 | var model = this.model 15 | var mc = model.count 16 | var dom = this.element.dom 17 | var vc = dom.childNodes.length 18 | if (this.trace) 19 | log("Datalist reset, model count:" + mc + ", current option count: " + vc) 20 | 21 | if (vc > mc) { 22 | var elements = [].slice.call(dom.childNodes); 23 | var removed = elements.splice(mc, vc - mc); 24 | removed.forEach(function(element) { 25 | dom.removeChild(element) 26 | }) 27 | } else if (vc < mc) { 28 | this._onRowsInserted(vc, mc) 29 | } 30 | this._onRowsChanged(0, Math.min(vc, mc)) 31 | } 32 | 33 | /// @private 34 | function _onRowsInserted(begin, end) { 35 | if (begin >= end) 36 | return 37 | if (this.trace) 38 | log("DataList::onRowsInserted", begin, end) 39 | 40 | var model = this.model 41 | var dom = this.element.dom 42 | var n = dom.childNodes.length 43 | if (begin > n) 44 | throw new Error("invalid begin in rowsInserted " + begin + "/" + n) 45 | if (begin < n) { 46 | var lastChild = dom.childNodes[begin] 47 | for(var i = begin; i < end; ++i) { 48 | dom.insertBefore(this._createValue(model.get(i)), lastChild) 49 | } 50 | } else { 51 | for(var i = begin; i < end; ++i) { 52 | dom.append(this._createValue(model.get(i))) 53 | } 54 | } 55 | } 56 | 57 | /// @private 58 | function _onRowsChanged(begin, end) { 59 | if (begin >= end) 60 | return 61 | if (this.trace) 62 | log("DataList::onRowsChanged", begin, end) 63 | 64 | var model = this.model 65 | var dom = this.element.dom 66 | var valueProperty = this.valueProperty 67 | for(var i = begin; i < end; ++i) { 68 | this._updateValue(dom.childNodes[i], model.get(i)) 69 | } 70 | } 71 | 72 | /// @private 73 | function _onRowsRemoved(begin, end) { 74 | if (begin >= end) 75 | return 76 | if (this.trace) 77 | log("DataList::onRowsRemoved", begin, end) 78 | 79 | var dom = this.element.dom 80 | for(var i = begin; i < end; ++i) { 81 | dom.removeChild(dom.childNodes[begin]); 82 | } 83 | } 84 | 85 | /// @private 86 | function addChild (child) { 87 | throw new Error(this.componentName + " can't have any children. They will be overwritten by model update") 88 | } 89 | } -------------------------------------------------------------------------------- /web/EllipsisText.qml: -------------------------------------------------------------------------------- 1 | /// Text item but with ellipsis when text painted width reached the item width 2 | Text { 3 | property bool overflowDetected; ///< overflow dectecting flag appears when text width is larger than item width 4 | property bool active: true; ///< flag for turning ellipsis mode on/off 5 | clip: true; 6 | 7 | updateStyle: { this.style('text-overflow', this.active ? 'ellipsis' : 'clip') } 8 | 9 | onActiveChanged: { this.updateStyle() } 10 | 11 | onCompleted: { this.updateStyle() } 12 | 13 | onPaintedWidthChanged: { 14 | var pw = value 15 | this.overflowDetected = value > this.width 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /web/GifSpinner.qml: -------------------------------------------------------------------------------- 1 | Image { 2 | source: "res/spinner.gif"; 3 | 4 | start: { 5 | this.visible = true; 6 | } 7 | 8 | stop: { 9 | this.visible = false; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /web/H1.qml: -------------------------------------------------------------------------------- 1 | Text { 2 | wrapMode: Text.WordWrap; 3 | font.family: "Roboto Slab"; 4 | 5 | /// returns tag for corresponding element 6 | function getTag() { return 'h1' } 7 | 8 | function registerStyle(style, tag) { 9 | style.addRule(tag, { 10 | "position": "absolute", 11 | "visibility": "inherit", 12 | "margin": "0px" 13 | }) 14 | } 15 | } -------------------------------------------------------------------------------- /web/H2.qml: -------------------------------------------------------------------------------- 1 | Text { 2 | wrapMode: Text.WordWrap; 3 | font.family: "Roboto Slab"; 4 | 5 | /// returns tag for corresponding element 6 | function getTag() { return 'h2' } 7 | 8 | function registerStyle(style, tag) { 9 | style.addRule(tag, { 10 | "position": "absolute", 11 | "visibility": "inherit", 12 | "margin": "0px" 13 | }) 14 | } 15 | } -------------------------------------------------------------------------------- /web/H3.qml: -------------------------------------------------------------------------------- 1 | Text { 2 | wrapMode: Text.WordWrap; 3 | font.family: "Roboto Slab"; 4 | 5 | /// returns tag for corresponding element 6 | function getTag() { return 'h3' } 7 | 8 | function registerStyle(style, tag) { 9 | style.addRule(tag, { 10 | "position": "absolute", 11 | "visibility": "inherit", 12 | "margin": "0px" 13 | }) 14 | } 15 | } -------------------------------------------------------------------------------- /web/H4.qml: -------------------------------------------------------------------------------- 1 | Text { 2 | wrapMode: Text.WordWrap; 3 | 4 | /// returns tag for corresponding element 5 | function getTag() { return 'h4' } 6 | 7 | function registerStyle(style, tag) { 8 | style.addRule(tag, { 9 | "position": "absolute", 10 | "visibility": "inherit", 11 | "margin": "0px" 12 | }) 13 | } 14 | } -------------------------------------------------------------------------------- /web/H5.qml: -------------------------------------------------------------------------------- 1 | Text { 2 | wrapMode: Text.WordWrap; 3 | 4 | /// returns tag for corresponding element 5 | function getTag() { return 'h5' } 6 | 7 | function registerStyle(style, tag) { 8 | style.addRule(tag, { 9 | "position": "absolute", 10 | "visibility": "inherit", 11 | "margin": "0px" 12 | }) 13 | } 14 | } -------------------------------------------------------------------------------- /web/H6.qml: -------------------------------------------------------------------------------- 1 | Text { 2 | wrapMode: Text.WordWrap; 3 | 4 | /// returns tag for corresponding element 5 | function getTag() { return 'h6' } 6 | 7 | function registerStyle(style, tag) { 8 | style.addRule(tag, { 9 | "position": "absolute", 10 | "visibility": "inherit", 11 | "margin": "0px" 12 | }) 13 | } 14 | } -------------------------------------------------------------------------------- /web/IFrame.qml: -------------------------------------------------------------------------------- 1 | /// iframe item to embed other page 2 | Item { 3 | signal loaded; ///< page was loaded signal 4 | property string source; ///< another page source URL 5 | property string origin; ///< readonly property of source's origin for message matching 6 | signal message; ///< message from window opened in this IFrame 7 | 8 | ///@private 9 | function getTag() { return 'iframe' } 10 | /// Sends message to window 11 | 12 | function postMessage(data) { 13 | this.element.dom.contentWindow.postMessage(data, this.source) 14 | } 15 | 16 | context.onMessage(event): { 17 | if (event.origin !== this.origin) //not ours 18 | return 19 | 20 | log('IFrame: incoming message from ' + event.origin) 21 | this.message(event) 22 | } 23 | 24 | onSourceChanged: { 25 | this.origin = new URL(value).origin 26 | this.element.dom.src = value; 27 | } 28 | 29 | constructor: { 30 | var self = this 31 | this.element.on('load', function() { self.source = this.dom.src; self.loaded() }) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /web/ImageSet.qml: -------------------------------------------------------------------------------- 1 | Item { 2 | property string srcSet; 3 | 4 | function getTag() { return 'img' } 5 | 6 | onSrcSetChanged: { 7 | this.element.setAttribute('srcset', value) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /web/LanguageSelector.qml: -------------------------------------------------------------------------------- 1 | Item { 2 | id: languageSelectorProto; 3 | property color delegateColor; 4 | property color delegateTextColor; 5 | property string currentLangCode; 6 | width: 50; 7 | height: 50; 8 | 9 | Image { 10 | id: currentLanguageIcon; 11 | anchors.fill: parent; 12 | } 13 | 14 | MouseArea { 15 | anchors.fill: parent; 16 | verticalSwipable: false; 17 | horizontalSwipable: false; 18 | 19 | onClicked: { 20 | innerLangView.visible = !innerLangView.visible 21 | } 22 | } 23 | 24 | ListModel { 25 | id: languageModel; 26 | 27 | onCountChanged: { 28 | if (value == 1) 29 | languageSelectorProto.setLanguageByIndex(0) 30 | } 31 | } 32 | 33 | ListView { 34 | id: innerLangView; 35 | width: 200; 36 | height: contentHeight; 37 | anchors.top: parent.top; 38 | anchors.right: parent.left; 39 | visible: false; 40 | model: languageModel; 41 | delegate: WebItem { 42 | property int index: model.index; 43 | width: parent.width; 44 | height: 50; 45 | 46 | Rectangle { 47 | anchors.fill: parent; 48 | color: languageSelectorProto.delegateColor; 49 | } 50 | 51 | Text { 52 | anchors.centerIn: parent; 53 | text: model.title; 54 | color: languageSelectorProto.delegateTextColor; 55 | font.pixelSize: parent.height - 20; 56 | } 57 | 58 | onClicked: { languageSelectorProto.setLanguageByIndex(this.index) } 59 | } 60 | } 61 | 62 | addLanguage(code, title, icon): { languageModel.append({ "code": code, "title": title, "icon": icon }) } 63 | 64 | setLanguageByIndex(index): { 65 | innerLangView.visible = false 66 | if (index >= 0 && index < languageModel.count) { 67 | currentLanguageIcon.source = languageModel.get(index).icon 68 | languageSelectorProto.currentLangCode = languageModel.get(index).code 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /web/LanguageSwitcher.qml: -------------------------------------------------------------------------------- 1 | WebItem { 2 | signal languageChanged; 3 | property variant languges; 4 | property int currentIndex; 5 | property int count; 6 | width: 50; 7 | height: 50; 8 | 9 | Image { 10 | id: currentIcon; 11 | anchors.fill: parent; 12 | } 13 | 14 | addLanguage(code, title, icon): { 15 | this.languges.push({ "code": code, "title": title, "icon": icon }) 16 | ++this.count 17 | } 18 | 19 | onCountChanged: { 20 | if (this.count == 1 && !this.currentIndex) 21 | currentIcon.source = this.languges[0].icon 22 | } 23 | 24 | onClicked: { this.currentIndex = (this.currentIndex + 1) % this.languges.length } 25 | 26 | onCurrentIndexChanged: { 27 | if (value >= 0 && value < this.count) 28 | currentIcon.source = this.languges[this.currentIndex].icon 29 | } 30 | 31 | onCompleted: { this.languges = [] } 32 | } 33 | -------------------------------------------------------------------------------- /web/MaterialIcon.qml: -------------------------------------------------------------------------------- 1 | /// google material icon control 2 | Text { 3 | property alias icon: text; ///< material icon name 4 | property alias size: font.pixelSize; ///< material icon size 5 | font.family: "Material Icons"; ///< @private 6 | 7 | constructor: { 8 | if (_globals.html5 && _globals.html5.html) 9 | _globals.html5.html.loadExternalStylesheet("https://fonts.googleapis.com/icon?family=Material+Icons") 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /web/Navbar.qml: -------------------------------------------------------------------------------- 1 | Layout { 2 | property string position; 3 | property bool vertical: width < 600; 4 | property bool collapsed: true; 5 | spacing: vertical ? 5 : 30; 6 | height: vertical && collapsed ? 50 : contentHeight; 7 | clip: true; 8 | 9 | onPositionChanged: { 10 | this.style('position', value) 11 | } 12 | 13 | addChild(child): { 14 | _globals.core.Item.prototype.addChild.apply(this, arguments) 15 | child.onChanged('height', this._layout.bind(this)) 16 | child.onChanged('width', this._layout.bind(this)) 17 | child.onChanged('recursiveVisible', this._layout.bind(this)) 18 | } 19 | 20 | _layout: { 21 | // log ("Navbar layout called"); 22 | var children = this.children; 23 | var cX = this.anchors.leftMargin, cY = 0, xMax = 0, yMax = 0; 24 | for(var i = 0; i < children.length; ++i) { 25 | var c = children[i] 26 | 27 | if (this.vertical) 28 | { 29 | if (c.recursiveVisible && c instanceof _globals.controls.web.NavbarItem) { 30 | c.x = cX + c.anchors.leftMargin; 31 | c.y = cY + c.anchors.topMargin; 32 | cY = c.y + c.height + this.spacing; 33 | xMax = xMax > c.width ? xMax : c.width; 34 | yMax = cY; 35 | } 36 | } 37 | else {//horizontal 38 | if (c.recursiveVisible && c instanceof _globals.controls.web.NavbarItem) { 39 | c.y = 0; 40 | c.x = cX + c.anchors.leftMargin; 41 | cX = c.x + c.width + this.spacing; 42 | yMax = yMax > c.height ? yMax : c.height; 43 | xMax = cX; 44 | } 45 | } 46 | } 47 | this.contentHeight = yMax; 48 | this.contentWidth = xMax; 49 | } 50 | 51 | onSpacingChanged: { this._layout(); } 52 | onVerticalChanged: { this._layout(); } 53 | } 54 | -------------------------------------------------------------------------------- /web/NavbarItem.qml: -------------------------------------------------------------------------------- 1 | WebItem { 2 | 3 | } -------------------------------------------------------------------------------- /web/NavigationMenuButton.qml: -------------------------------------------------------------------------------- 1 | Item { 2 | property string icon; 3 | property bool scrollable: false; 4 | property bool show: false; 5 | property Item content; 6 | property int contentX; 7 | property int contentY; 8 | property Rectangle panel: Rectangle { 9 | width: 300; 10 | height: 300; 11 | color: "#37474F"; 12 | visible: false; 13 | } 14 | width: 50; 15 | height: 50; 16 | 17 | Rectangle { 18 | id: navigationContentPanel; 19 | width: parent.panel.width; 20 | height: parent.panel.height; 21 | anchors.top: parent.bottom; 22 | anchors.right: parent.right; 23 | anchors.margins: 5; 24 | color: parent.panel.color; 25 | radius: parent.panel.radius; 26 | effects.shadow.blur: 6; 27 | effects.shadow.color: "#0009"; 28 | effects.shadow.y: 1; 29 | visible: parent.show; 30 | clip: true; 31 | 32 | content: Item { 33 | anchors.fill: parent; 34 | anchors.topMargin: parent.parent.contentY; 35 | anchors.leftMargin: parent.parent.contentX; 36 | } 37 | 38 | onCompleted: { 39 | if (this.parent.scrollable) { 40 | this.style('overflow-x', 'hidden') 41 | this.style('overflow-y', 'scroll') 42 | } 43 | 44 | var item = this.parent.content 45 | item.width = this.content.width; 46 | item.height = this.content.height; 47 | this.content.element.append(item.element) 48 | } 49 | } 50 | 51 | Rectangle { 52 | width: 23; 53 | height: width; 54 | color: parent.panel.color; 55 | anchors.right: navigationContentPanel.right; 56 | anchors.bottom: navigationContentPanel.top; 57 | anchors.rightMargin: 10; 58 | anchors.bottomMargin: -height / 2 - 5; 59 | visible: navigationContentPanel.visible; 60 | rotate: 45; 61 | } 62 | 63 | WebItem { 64 | anchors.fill: parent; 65 | anchors.margins: 5; 66 | 67 | Image { 68 | anchors.fill: parent; 69 | source: parent.parent.icon; 70 | fillMode: Image.PreserveAspectFit; 71 | } 72 | 73 | onClicked: { this.parent.toggleShow() } 74 | } 75 | 76 | hide: { this.show = false } 77 | toggleShow: { this.show = !this.show } 78 | onShowChanged: { if (value) navigationContentPanel.setFocus() } 79 | } 80 | -------------------------------------------------------------------------------- /web/ResizableBox.qml: -------------------------------------------------------------------------------- 1 | Rectangle { 2 | id: resizable; 3 | property int maxWidth; 4 | property int maxHeight; 5 | property int minHeight: 1; 6 | property int minWidth: 1; 7 | color: "#BBDEFB"; 8 | border.width: 1; 9 | border.color: "gray"; 10 | 11 | Item { 12 | id: resizer; 13 | width:0; 14 | height:0; 15 | y: parent.height; 16 | x: parent.width; 17 | z: 1; 18 | property Mixin hover: HoverClickMixin { cursor: "se-resize";} 19 | property Mixin drag: DragMixin { 20 | bottom: parent.parent.maxHeight; 21 | top: parent.parent.minHeight; 22 | right: parent.parent.maxWidth; 23 | left: parent.parent.minWidth; 24 | } 25 | 26 | onXChanged: { this.parent.width = value; } 27 | onYChanged: { this.parent.height = value; } 28 | 29 | Rectangle { 30 | x: -15; y: -15; 31 | width: 24; height: 24; 32 | radius: 12; 33 | color: "#607D8B"; 34 | opacity: parent.hover.value ? 0.9 : 0.2; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /web/Script.qml: -------------------------------------------------------------------------------- 1 | Item { 2 | property string source; ///< source to load script from 3 | 4 | signal loaded; ///< signal emitted when script loaded 5 | 6 | constructor: { 7 | this.element.setAttribute('type', 'text/javascript') 8 | this._onLoad = this._context.wrapNativeCallback(function() { 9 | this._loaded = true 10 | this.loaded(); 11 | }.bind(this)) 12 | this._loaded = false 13 | this.element.dom.addEventListener('load', this._onLoad) 14 | } 15 | 16 | function on (name, callback) { 17 | _globals.core.Item.prototype.on.call(this, name, callback) 18 | if (this._loaded && name === 'loaded') 19 | callback() 20 | } 21 | 22 | ///@private 23 | function discard() { 24 | this._loaded = false 25 | this.element.dom.removeEventListener('load', this._onLoad) 26 | _globals.core.Item.prototype.discard.call(this) 27 | } 28 | 29 | /// @private 30 | function _delayedLoad() { 31 | this._context.delayedAction('script:load', this, this.load) 32 | } 33 | 34 | function load() { 35 | this._loaded = false 36 | var source = this.source 37 | if (!source) 38 | return 39 | 40 | log('loading script from ' + source) 41 | 42 | this.element.setAttribute('src', source) 43 | } 44 | 45 | onSourceChanged: { this._delayedLoad() } 46 | onCompleted: { this._delayedLoad() } 47 | 48 | function getTag() { return 'script' } 49 | } 50 | -------------------------------------------------------------------------------- /web/Sharer.qml: -------------------------------------------------------------------------------- 1 | ///object for sharing URL in corresponded social network 2 | Object { 3 | property enum socailNetwork { GPlus, Facebook, Vk, Twitter }; ///< social network name 4 | property string url; ///< URL you want to share 5 | 6 | onSocailNetworkChanged: { 7 | switch(value) { 8 | case this.GPlus: this.href = 'https://plus.google.com/share?url='; break 9 | case this.Facebook: this.href = 'https://www.facebook.com/sharer/sharer.php?s=100&p%5Burl%5D='; break 10 | case this.Vk: this.href = 'https://vkontakte.ru/share.php?url='; break 11 | case this.Twitter: this.href = 'https://twitter.com/share?url='; break 12 | } 13 | } 14 | 15 | ///load URL 16 | open: { 17 | if (!this.url) { 18 | log("url is undefined!") 19 | return 20 | } 21 | window.open(this.href + this.url + '%2Fauth%2Flogin') 22 | } 23 | 24 | ///set location to corresponded URL 25 | redirect: { 26 | if (!this.url) { 27 | log("url is undefined!") 28 | return 29 | } 30 | window.location = this.href + this.url + '%2Fauth%2Flogin' 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /web/Spinner.qml: -------------------------------------------------------------------------------- 1 | WebItem { 2 | id: spinnerItem; 3 | property bool running; 4 | width: 80; 5 | height: 80; 6 | transform.rotate: 0; 7 | property int squeeze: 0; 8 | property int squeezeX: squeeze * width * 5 / 80; 9 | property int squeezeY: squeeze * height * 5 / 80; 10 | 11 | start: { 12 | this.running = true; 13 | } 14 | 15 | stop: { 16 | this.running = false; 17 | } 18 | 19 | onRunningChanged: { 20 | spinnerItem.transform.rotate += 360; 21 | spinnerItem.squeeze = ++spinnerItem.squeeze % 5; 22 | } 23 | 24 | Rectangle { 25 | id: r1; 26 | x: parent.squeezeX; 27 | y: parent.squeezeY; 28 | width: spinnerItem.width * 3/8; 29 | height: spinnerItem.height * 3/8; 30 | radius: width / 2; 31 | color: "#FFEB3B"; 32 | opacity: 0.8; 33 | Behavior on x, y { Animation {duration: 400; easing: "linear"; cssTransition: true;} } 34 | } 35 | 36 | Rectangle { 37 | id: r2; 38 | x: parent.width * 5/8 - parent.squeezeX; 39 | y: parent.squeezeY; 40 | width: spinnerItem.width * 3/8; 41 | height: spinnerItem.height * 3/8; 42 | radius: width / 2; 43 | color: "#4CAF50"; 44 | opacity: 0.8; 45 | Behavior on x, y { Animation {duration: 400; easing: "linear"; cssTransition: true;} } 46 | } 47 | 48 | Rectangle { 49 | x: parent.squeezeX; 50 | y: spinnerItem.height * 5/8 - parent.squeezeY; 51 | id: r3; 52 | width: spinnerItem.width * 3/8; 53 | height: spinnerItem.height * 3/8; 54 | radius: width / 2; 55 | color: "#2196F3"; 56 | opacity: 0.8; 57 | Behavior on x, y { Animation {duration: 400; easing: "linear"; cssTransition: true;} } 58 | } 59 | 60 | Rectangle { 61 | x: spinnerItem.height * 5/8 - parent.squeezeX; 62 | y: spinnerItem.height * 5/8 - parent.squeezeY; 63 | id: r4; 64 | width: spinnerItem.width * 3/8; 65 | height: spinnerItem.height * 3/8; 66 | radius: width / 2; 67 | z: 1; 68 | color: "#F44336"; 69 | opacity: 0.8; 70 | Behavior on x, y { Animation {duration: 400; easing: "linear"; cssTransition: true;} } 71 | } 72 | 73 | Behavior on transform { Animation {duration: 850; cssTransition: true;} } 74 | 75 | Timer { 76 | id: spinTimer; 77 | running: parent.running && spinnerItem.recursiveVisible; 78 | interval: 600; 79 | repeat: true; 80 | 81 | onTriggered: { 82 | spinnerItem.transform.rotate += 360; 83 | spinnerItem.squeeze = ++spinnerItem.squeeze % 5; 84 | if (spinnerItem.squeeze === 1) { 85 | var t = r1.z; 86 | r1.z = r2.z; r2.z = r3.z; r3.z = r4.z; r4.z = t; 87 | } 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /web/WebItem.qml: -------------------------------------------------------------------------------- 1 | ///clickable and hoverable rectangle for mouse supportings applications 2 | Rectangle { 3 | property Mixin hoverMixin: HoverClickMixin {} ///< hover mixin object proveds common API for mouse properties 4 | property alias hover: hoverMixin.value; ///< mouse hovered flag 5 | property alias clickable: hoverMixin.clickable; ///< flag which enables or disables mouse click event handling 6 | property alias hoverable: hoverMixin.enabled; ///< flag which enables or disables mouse hover event handling 7 | property alias cursor: hoverMixin.cursor; ///< mouse cursor property 8 | property alias activeHover: hoverMixin.activeHover; ///< flag which becames 'true' on 'mouseover' event and becames 'false' on 'mouseout' event 9 | property alias activeHoverEnabled: hoverMixin.activeHoverEnabled; ///< flag which enables or disables 'mouseover' and 'mouseout' event handling 10 | color: "transparent"; ///< background color 11 | hoverMixin.cursor: "pointer"; 12 | property string position; ///< position mode property 13 | 14 | /// @private 15 | onPositionChanged: { this.style('position', value); } 16 | } 17 | -------------------------------------------------------------------------------- /web/WebLink.qml: -------------------------------------------------------------------------------- 1 | WebItem { 2 | property string href; 3 | property string target; 4 | 5 | onHrefChanged: { this.element.setAttribute('href', value); } 6 | onTargetChanged: { this.element.setAttribute('target', value); } 7 | 8 | function getTag() { return 'a' } 9 | 10 | function registerStyle(style, tag) { 11 | style.addRule(tag, { 12 | "text-decoration": "none", 13 | "position": "absolute", 14 | "visibility": "inherit", 15 | "border-style": "solid", 16 | "border-width": "0px", 17 | "white-space": "nowrap", 18 | "border-radius": "0px", 19 | "opacity": 1.0, 20 | "transform": "none", 21 | "left": "0px", 22 | "top": "0px", 23 | "width": "0px", 24 | "height": "0px" 25 | }) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /web/api/Method.qml: -------------------------------------------------------------------------------- 1 | /** 2 | Declarative REST API method declaration component. 3 | Each instance registered as javascript method with name specified in name property. 4 | You have to override args function if you need to provide json body for your request (normally needed for POST/PUT) 5 | 6 | This is to hide specific API details from method arguments. 7 | E.g. if you have login method with name/password, you can specify it as 8 | function args(name, password) { return { username: name, password: password}} 9 | method registered with args functions will have additional arguments, e.g login(name, pass, done, error) 10 | */ 11 | Object { 12 | property string type: "GET"; ///< method type, could be any of supported HTTP request types 13 | property string name; ///< method name 14 | property string path; 15 | property int timeout; 16 | 17 | /// @private call implementation 18 | function call(api, args) { 19 | var nargs = this.args.length 20 | if (args.length < nargs + 2) 21 | throw new Error("not enough arguments for method " + this.name) 22 | nargs = args.length - 2 23 | 24 | var argsargs = Array.prototype.slice.call(args, 0, nargs) 25 | var data = this.args.apply(this, argsargs) 26 | var callback = args[nargs + 0] 27 | var error = args[nargs + 1] 28 | var headers = {} 29 | var newHeaders = this.headers(headers) 30 | if (newHeaders !== undefined) 31 | headers = newHeaders 32 | var path = this.pathArgs(this.path, argsargs) 33 | 34 | api.call(path, callback, error, this.type, data, headers, this.timeout) 35 | } 36 | 37 | /// headers override 38 | function headers(headers) { 39 | } 40 | 41 | /// additional arguments provided for method 42 | function args() { 43 | } 44 | 45 | /// @internal replace path arguments ({} by default) 46 | function pathArgs(path, args) { 47 | var path = this.path 48 | var re = /\{(\w+)\}/g 49 | var index = 0 50 | path = path.replace(re, function(m) { 51 | return (index < args.length)? args[index++]: '' 52 | }) 53 | 54 | return path 55 | } 56 | 57 | onNameChanged: { 58 | this.parent._registerMethod(this.name, this) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /web/api/Rest.qml: -------------------------------------------------------------------------------- 1 | /** 2 | Root component for REST API declaration. 3 | Normally Rest component contains one or more Method instances. 4 |
  5 | 		Rest {
  6 | 			id: api;
  7 | 			baseUrl: "https://example.com/v1";
  8 | 
  9 | 			function headers(headers) { headers.token = 'secret'; }
 10 | 
 11 | 			Method { name: "getList"; path: "list/{name}"; }
 12 | 		}
 13 | 		//in js:
 14 | 		api.getList(name, function() {...}, function () { ... })
 15 | 	
16 | */ 17 | 18 | 19 | Object { 20 | Request { id: apiRequest; } ///< Request object used for ajax requests 21 | 22 | property string baseUrl; ///< base url for all requests 23 | 24 | signal error; ///< all errors signalled here 25 | signal internetConnectionLost; ///< some platforms signal when internet connection lost, see onError 26 | property int activeRequests; ///< number of currently running requests. 27 | 28 | constructor: { 29 | this._methods = {} 30 | } 31 | 32 | onError(url, method, response): { 33 | if ((typeof window !== 'undefined' && !window.navigator.onLine) || response && response.target && response.target.status === 0 && response.target.response === "") { 34 | this.internetConnectionLost({ "url": url, "method": method, "response": response }) 35 | } 36 | } 37 | 38 | /// args function allows to override arguments for all methods, e.g. adding session token 39 | function args(args) { 40 | return args 41 | } 42 | 43 | /// headers function allows to override headers for all methods, e.g. adding session token 44 | function headers(headers) { 45 | } 46 | 47 | /// @private calls invokes args, headers and ajax, then processes result 48 | function _call(name, callback, error, method, data, head, timeout) { 49 | var headers = head || {} 50 | 51 | if (data) { 52 | data = this.args(data) 53 | headers["Content-Type"] = "application/json" 54 | } 55 | 56 | var newHeaders = this.headers(headers) 57 | if (newHeaders !== undefined) 58 | headers = newHeaders 59 | 60 | ++this.activeRequests 61 | var url = name 62 | var self = this 63 | 64 | apiRequest.ajax({ 65 | method: method || "GET", 66 | headers: headers, 67 | contentType: 'application/json', 68 | settings: { 69 | timeout: timeout, 70 | }, 71 | url: url, 72 | data: data, 73 | done: function(res) { 74 | --self.activeRequests 75 | if (res.target && res.target.status >= 400) { 76 | log("Error in request", res) 77 | if (error) 78 | error(res) 79 | self.error({"url": url, "method": method, "response": res}) 80 | return 81 | } 82 | 83 | var text = res.target.responseText 84 | if (!text) { 85 | callback("") 86 | return 87 | } 88 | var res 89 | try { 90 | res = JSON.parse(text) 91 | } catch (e) { 92 | res = text 93 | } 94 | callback(res) 95 | }, 96 | error: function(res) { 97 | --self.activeRequests 98 | if (error) 99 | error(res) 100 | self.error({"url": url, "method": method, "response": res}) 101 | } 102 | }) 103 | } 104 | 105 | /// @internal top-level call implementation 106 | function call(name, callback, error, method, data, head, timeout) { 107 | if (name.indexOf('://') < 0) { 108 | var baseUrl = this.baseUrl 109 | if (baseUrl[baseUrl.length - 1] === '/' || name[0] === '/') 110 | name = baseUrl + name 111 | else 112 | name = baseUrl + '/' + name 113 | } 114 | this._call(name, callback, error, method, JSON.stringify(data), head, timeout) 115 | } 116 | 117 | /// @private method registration 118 | function _registerMethod(name, method) { 119 | if (!name) 120 | return 121 | 122 | var api = this 123 | this[name] = function() { 124 | method.call(api, arguments) 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /web/audio/AudioBuffer.qml: -------------------------------------------------------------------------------- 1 | /// Object, which can then be populated by data, and played via an AudioBuffer 2 | Object { 3 | property int length; ///< Read only integer property representing the length, in sample-frames, of the PCM data stored in the buffer. 4 | property int channelsCount; ///< Returns an integer representing the number of discrete audio channels described by the PCM data stored in the buffer. 5 | property real sampleRate; ///< Read only real property representing the sample rate, in samples per second, of the PCM data stored in the buffer. 6 | property real duration; ///< Read only real property representing the duration, in seconds, of the PCM data stored in the buffer. 7 | 8 | ///@private 9 | onLengthChanged, onChannelsCountChanged: { this.reset() } 10 | 11 | /// Play audio buffer 12 | play: { this._source.start(); } 13 | 14 | /// Set audio buffer 15 | setBuffer(data): { this._data = data; this.length = data.length } 16 | 17 | ///@private 18 | reset: { 19 | var channels = this.channelsCount; 20 | var audioCtx = this._audioCtx 21 | var frameCount = audioCtx.sampleRate * channels; 22 | var myArrayBuffer = audioCtx.createBuffer(channels, frameCount, audioCtx.sampleRate); 23 | var source = audioCtx.createBufferSource(); 24 | source.buffer = this._data; 25 | source.connect(audioCtx.destination); 26 | this._source = source 27 | 28 | this.sampleRate = audioCtx.sampleRate 29 | } 30 | 31 | ///@private 32 | constructor: { 33 | if (window.AudioContext || window.webkitAudioContext) { 34 | this._audioCtx = new (window.AudioContext || window.webkitAudioContext)(); 35 | this.reset(); 36 | } else { 37 | log("Web Audio API not supported") 38 | return 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /web/audio/Oscillator.qml: -------------------------------------------------------------------------------- 1 | /// Object generates sound and play it via web audio API 2 | Object { 3 | property enum type { Sine, Square, Sawtooth, Triangle }; ///< signal type enum: Sine, Square, Sawtooth or Triangle 4 | property bool playing; ///< is oscillator playing right now 5 | property int frequency; ///< signal frequency value 6 | property int detune; ///< representing detuning of oscillation in cents 7 | 8 | Timer { 9 | id: timeOutTimer; 10 | 11 | start(timeOut): { 12 | this.interval = timeOut 13 | this.restart() 14 | } 15 | 16 | onTriggered: { this.parent.pause() } 17 | } 18 | 19 | ///@private 20 | onDetuneChanged: { this._oscillator.frequency.value = value; } 21 | 22 | ///@private 23 | onFrequencyChanged: { this._oscillator.detune.value = value; } 24 | 25 | ///@private 26 | onTypeChanged: { 27 | var oscillator = this._oscillator 28 | switch(value) { 29 | case this.Sine: oscillator.type = 'sine'; break 30 | case this.Square: oscillator.type = 'square'; break 31 | case this.Sawtooth: oscillator.type = 'sawtooth'; break 32 | case this.Triangle: oscillator.type = 'triangle'; break 33 | } 34 | } 35 | 36 | ///< paly generated sound 37 | play: { 38 | if (this.playing) 39 | return 40 | this._oscillator.start() 41 | this.playing = true 42 | } 43 | 44 | ///< pause generated sound 45 | pause: { 46 | if (!this.playing) 47 | return 48 | this._oscillator.stop() 49 | this.playing = false 50 | } 51 | 52 | ///< play/pause generated sound 53 | playPause: { this.playing ? this.pause() : this.play() } 54 | 55 | /**@param ms:int time interval in milliseconds 56 | play generated sound 'ms' milliseconds*/ 57 | playWithTimeout(ms): { 58 | this.play() 59 | timeOutTimer.start(ms) 60 | } 61 | 62 | ///@private 63 | onPlayingChanged: { 64 | if (value) 65 | return 66 | delete this._oscillator 67 | this._oscillator = this._audioCtx.createOscillator() 68 | this._oscillator.connect(this._audioCtx.destination) 69 | this.reset() 70 | } 71 | 72 | ///@private 73 | constructor: { 74 | if (window.AudioContext || window.webkitAudioContext) { 75 | this._audioCtx = new (window.AudioContext || window.webkitAudioContext)(); 76 | this._oscillator = this._audioCtx.createOscillator() 77 | this._oscillator.connect(this._audioCtx.destination) 78 | } else { 79 | log("Web Audio API not supported") 80 | return 81 | } 82 | } 83 | 84 | ///@private 85 | reset: { 86 | var oscillator = this._oscillator 87 | oscillator.frequency.value = this.frequency 88 | oscillator.detune.value = this.detune 89 | switch(this.type) { 90 | case this.Sine: oscillator.type = 'sine' 91 | case this.Square: oscillator.type = 'square' 92 | case this.Sawtooth: oscillator.type = 'sawtooth' 93 | case this.Triangle: oscillator.type = 'triangle' 94 | } 95 | } 96 | 97 | ///@private 98 | onCompleted: { this.reset() } 99 | } 100 | -------------------------------------------------------------------------------- /web/gl/FragmentShader.qml: -------------------------------------------------------------------------------- 1 | Shader { 2 | /**@param {string} sourceCode - shader source code 3 | fragment shader creation function*/ 4 | create(sourceCode): { 5 | try { 6 | return this.createImpl(sourceCode, this._gl.FRAGMENT_SHADER) 7 | } catch(e) { 8 | return null 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /web/gl/GlContext.qml: -------------------------------------------------------------------------------- 1 | /// Canvas with initialized WebGl indeside 2 | Canvas { 3 | signal drawScene; ///< triggered on scene rendering with time delta argument 4 | 5 | constructor: { 6 | var names = ["webgl", "experimental-webgl", "webkit-3d", "moz-webgl"] 7 | var gl = null 8 | for (var i = 0; i < names.length; ++i) { 9 | try { 10 | gl = this.getContext(names[i]); 11 | } catch (e) { 12 | log("Failed to create GL context", e) 13 | } 14 | if (gl) 15 | break; 16 | } 17 | this.gl = gl 18 | } 19 | 20 | /// @private 21 | render(now): { 22 | now *= 0.001 23 | var deltaTime = now - this.then 24 | this.then = now 25 | this.drawScene(deltaTime) 26 | requestAnimationFrame(this.render.bind(this)) 27 | } 28 | 29 | onWidthChanged, 30 | onHeightChanged: { 31 | this.gl.viewport(0, 0, this.width, this.height) 32 | } 33 | 34 | /// @private 35 | startRender: { 36 | this.then = 0 37 | requestAnimationFrame(this.render.bind(this)) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /web/gl/Program.qml: -------------------------------------------------------------------------------- 1 | Object { 2 | constructor: { 3 | if (!this.parent || !this.parent.gl) { 4 | log("Place 'Program' in 'GlContext'") 5 | return 6 | } 7 | 8 | var gl = this.parent.gl 9 | this._program = gl.createProgram() 10 | this._gl = gl 11 | } 12 | 13 | attachShader(shader): { this._gl.attachShader(this._program, shader) } 14 | 15 | linkProgram: { this._gl.linkProgram(this._program) } 16 | 17 | create: { 18 | var gl = this._gl 19 | var program = this._program; 20 | if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { 21 | alert('Unable to initialize the shader program: ' + gl.getProgramInfoLog(program)) 22 | return null 23 | } 24 | return program 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /web/gl/Shader.qml: -------------------------------------------------------------------------------- 1 | Object { 2 | constructor: { 3 | if (!this.parent || !this.parent.gl) { 4 | log("Place 'VertexShader' in 'GlContext'") 5 | return 6 | } 7 | 8 | this._gl = this.parent.gl 9 | } 10 | 11 | /**@param {string} sourceCode - shader source code 12 | @param {string} type - shader type to create 13 | shader creation function implementaion*/ 14 | createImpl(sourceCode, type): { 15 | var gl = this._gl 16 | var shader = gl.createShader(type) 17 | gl.shaderSource(shader, sourceCode) 18 | gl.compileShader(shader) 19 | 20 | if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { 21 | var info = gl.getShaderInfoLog(shader) 22 | throw 'Could not compile WebGL program: ' + info 23 | } 24 | return shader 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /web/gl/VertexShader.qml: -------------------------------------------------------------------------------- 1 | Shader { 2 | /**@param {string} sourceCode - shader source code 3 | vertex shader creation function*/ 4 | create(sourceCode): { 5 | try { 6 | return this.createImpl(sourceCode, this._gl.VERTEX_SHADER) 7 | } catch(e) { 8 | return null 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /web/tables/Table.qml: -------------------------------------------------------------------------------- 1 | TableElement { 2 | function getTag() { return 'table' } 3 | 4 | property Object model: ListModel { } 5 | property Object headerModel: ListModel { } 6 | property Color textColor; 7 | 8 | onTextColorChanged: { this.style('color', _globals.core.Color.normalize(value)) } 9 | 10 | TableHeader { 11 | model: parent.headerModel; 12 | delegate: TableHeaderCell { text: model.text; } 13 | } 14 | 15 | TableBody { 16 | model: parent.model; 17 | delegate: TableRow { 18 | model: ListModel { data: model.data; } 19 | delegate: TableCell { text: model.value; } 20 | } 21 | } 22 | 23 | assign(data, columns): { 24 | log('table data', data, columns) 25 | var headerModel = this.headerModel 26 | columns.forEach(function(name) { headerModel.append({ text: name }) }) 27 | var rowData = [] 28 | data.forEach(function (srcRow) { 29 | var row = [] 30 | columns.forEach(function(name) { 31 | var value = srcRow[name] 32 | if (value !== undefined) 33 | row.push({ value: value }) 34 | else 35 | row.push('') 36 | }) 37 | rowData.push({ data: row}) 38 | }) 39 | log('table data', rowData) 40 | this.model.assign(rowData) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /web/tables/TableBody.qml: -------------------------------------------------------------------------------- 1 | Repeater { 2 | function getTag() { return 'tbody' } 3 | } 4 | -------------------------------------------------------------------------------- /web/tables/TableCell.qml: -------------------------------------------------------------------------------- 1 | TableElement { 2 | function getTag() { return 'td' } 3 | } 4 | -------------------------------------------------------------------------------- /web/tables/TableElement.qml: -------------------------------------------------------------------------------- 1 | Item { 2 | property string text; 3 | 4 | onTextChanged: { 5 | this.element.setHtml(this.text, this) 6 | } 7 | 8 | onCompleted: { 9 | this.element.setHtml(this.text, this) 10 | } 11 | 12 | function registerStyle(style, tag) { 13 | style.addRule(tag, { 14 | 'position': 'default', 15 | 'visibility': 'inherit', 16 | 'margin': '0px', 17 | 'padding': '0px', 18 | 'border-collapse': 'collapse' 19 | }); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /web/tables/TableFooter.qml: -------------------------------------------------------------------------------- 1 | TableElement { 2 | function getTag() { return 'tfoot' } 3 | } 4 | -------------------------------------------------------------------------------- /web/tables/TableHeader.qml: -------------------------------------------------------------------------------- 1 | Repeater { 2 | function getTag() { return 'thead' } 3 | } 4 | -------------------------------------------------------------------------------- /web/tables/TableHeaderCell.qml: -------------------------------------------------------------------------------- 1 | TableElement { 2 | function getTag() { return 'th' } 3 | } 4 | -------------------------------------------------------------------------------- /web/tables/TableRow.qml: -------------------------------------------------------------------------------- 1 | Repeater { 2 | function getTag() { return 'tr' } 3 | } 4 | -------------------------------------------------------------------------------- /web/websocket/WebSocketClient.qml: -------------------------------------------------------------------------------- 1 | /// Wrapper for a WebSocket 2 | /// The comments are copied or inspired by https://developer.mozilla.org/en-US/docs/Web/API/WebSocket 3 | Object { 4 | /// The URL to which to connect; this should be the URL to which the WebSocket server will respond. 5 | /// The url needs to start with either 'ws://' or 'wss://' 6 | property string url; 7 | 8 | /// 0 CONNECTING Socket has been created. The connection is not yet open. 9 | /// 1 OPEN The connection is open and ready to communicate. 10 | /// 2 CLOSING The connection is in the process of closing. 11 | /// 3 CLOSED The connection is closed or couldn't be opened. 12 | property enum state { Connecting, Open, Closing, Closed }: Closed; 13 | 14 | /// Emitted when a message is received from the server. 15 | signal message; 16 | /// Emitted when the state changes to 1 (Open); this indicates that the connection is ready to send and receive data. 17 | signal opened; 18 | /// Emitted when the socket was closed, the event has at least the following three members code, reason and wasClean 19 | signal closed; 20 | /// Emitted when an error occurs on the WebSocket. 21 | signal error; 22 | 23 | /// Method enqueues the specified data to be transmitted to the server over the WebSocket connection. 24 | send(msg): { 25 | if (this.state == this.Open) 26 | this._socket.send(msg) 27 | } 28 | 29 | /// Initiate connect to the provided url 30 | connect: { 31 | if (!this.url) { 32 | this.error("url not set") 33 | return 34 | } 35 | 36 | try { 37 | var socket = new WebSocket(this.url) 38 | this._socket = socket 39 | this.state = this.Connecting 40 | 41 | var context = this._context 42 | var self = this 43 | socket.onopen = context.wrapNativeCallback(function(event) { 44 | self.state = self.Open 45 | self.opened(event); 46 | }) 47 | 48 | socket.onclose = context.wrapNativeCallback(function(event) { 49 | self.state = self.Closed 50 | self.closed(event) 51 | }) 52 | 53 | socket.onerror = context.wrapNativeCallback(function(event) { 54 | self.error(event) 55 | }) 56 | 57 | socket.onmessage = context.wrapNativeCallback(function(event) { 58 | self.message(event.data) 59 | }) 60 | } catch(e) { 61 | this.error("connect error reason: " + e) 62 | } 63 | } 64 | 65 | /// Closes the WebSocket connection or connection attempt, if any. 66 | close: { 67 | if(!this._socket) 68 | return; 69 | this.state = self.Closing 70 | this._socket.close() 71 | } 72 | 73 | /// Returns the number of bytes of data that have been queued using calls to send() but not yet transmitted to the network. 74 | bufferedAmount: { 75 | return this._socket.bufferedAmount; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /web/websocket/WebSocketServer.qml: -------------------------------------------------------------------------------- 1 | Object { 2 | id: serverProto; 3 | signal message; 4 | signal userConnected; 5 | signal userDisconnected; 6 | property bool started; 7 | property bool autostart; 8 | property string ip; 9 | property string port; 10 | 11 | // TODO: use device ready event instead of timer 12 | Timer { 13 | id: startDelayTimer; 14 | interval: 2000; 15 | 16 | onTriggered: { this.parent.start() } 17 | } 18 | 19 | sendMessage(msg, name): { 20 | log("send:", this.started, "user", this._users) 21 | if (!this.started) 22 | return 23 | 24 | var users = this._users 25 | for (var i in users) 26 | this._wsserver.send(users[i], { "message": msg, "user": { "name": name } }) 27 | } 28 | 29 | start: { 30 | var context = this._context 31 | 32 | if (!window.cordova || !window.cordova.plugins) { 33 | log("WSS Plugin wasn't initialized") 34 | return 35 | } 36 | 37 | var wsserver = window.cordova.plugins.wsserver; 38 | var port = this.port 39 | var self = this 40 | var users = {} 41 | 42 | wsserver.start(port, { 43 | 'onFailure': context.wrapNativeCallback(function(addr, port, reason) { 44 | log('Stopped listening on %s:%d. Reason: %s', addr, port, reason); 45 | self.started = false 46 | }), 47 | 'onOpen': context.wrapNativeCallback(function(user) { 48 | log('A user connected:', user); 49 | users[user.uuid] = user 50 | self.userConnected(user) 51 | }), 52 | 'onMessage': context.wrapNativeCallback(function(user, msg) { 53 | self.message(msg, user) 54 | }), 55 | 'onClose': context.wrapNativeCallback(function(user, code, reason, wasClean) { 56 | log('A user disconnected from %s', user.remoteAddr); 57 | self.userDisconnected(user, code, reason, wasClean) 58 | }) 59 | }, context.wrapNativeCallback(function onStart(addr, port) { 60 | log('Listening on address', addr, "port", port); 61 | self.started = true 62 | }), context.wrapNativeCallback(function onDidNotStart(reason) { 63 | log('Did not start. Reason: %s', reason); 64 | self.started = false 65 | })); 66 | 67 | wsserver.getInterfaces(context.wrapNativeCallback(function(interfaces) { 68 | for (var i in interfaces) { 69 | log("Got interfaces", interfaces) 70 | var iface = interfaces[i] 71 | if (iface && iface.ipv4Addresses && iface.ipv4Addresses.length) { 72 | serverProto.ip = iface.ipv4Addresses[0] 73 | break 74 | } 75 | } 76 | })) 77 | 78 | this._wsserver = wsserver 79 | this._users = users 80 | } 81 | 82 | send(msg): { 83 | if (!this.started || !this._wsserver || !this._users) 84 | return 85 | for (var i in this._users) { 86 | var user = this._users[i] 87 | this._wsserver.send({'uuid': user.uuid}, msg) 88 | } 89 | } 90 | 91 | onCompleted: { if (this.autostart) startDelayTimer.restart() } 92 | } 93 | -------------------------------------------------------------------------------- /yandex/YandexMap.qml: -------------------------------------------------------------------------------- 1 | Item { 2 | property real centerLongitude; 3 | property real centerLatitude; 4 | property int zoom; 5 | 6 | onCenterLongitudeChanged, onCenterLatitudeChanged, onZoomChanged: { this._updateCenter() } 7 | 8 | constructor: { 9 | if (!window.YMaps) { 10 | log("YMaps not found! Probably you forgot to include yandex map api:") 11 | log('') 12 | return 13 | } 14 | this._api = window.YMaps 15 | var map = new this._api.Map(this.element.dom) 16 | this._map = map 17 | log("Map", map) 18 | } 19 | 20 | function _updateCenter() { 21 | if (!this._map) { 22 | log("Map not initizlized") 23 | return 24 | } 25 | var center = new this._api.GeoPoint(this.centerLongitude, this.centerLatitude); 26 | this._map.setCenter(center, this.zoom) 27 | } 28 | 29 | getUserLocation: { return this._api ? this._api.location : undefined } 30 | } 31 | --------------------------------------------------------------------------------