├── .gitignore ├── .gitmodules ├── .rspec ├── .travis.yml ├── .yardopts ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── changes.md ├── examples ├── assets │ └── fonts │ │ └── fontawesome-webfont.ttf ├── fizzbuzz │ ├── capture.png │ ├── fizzbuzz.rb │ └── main.qml ├── todo_array │ ├── capture.png │ ├── main.qml │ └── todo_array.rb ├── todo_sequel │ ├── capture.png │ ├── main.qml │ └── todo_sequel.rb └── twitter │ ├── capture.png │ ├── main.qml │ └── twitter.rb ├── ext └── qml │ ├── application.c │ ├── application.h │ ├── component.c │ ├── component.h │ ├── conversion.c │ ├── conversion.h │ ├── engine.c │ ├── engine.h │ ├── extconf.rb │ ├── js_array.c │ ├── js_array.h │ ├── js_function.c │ ├── js_function.h │ ├── js_object.c │ ├── js_object.h │ ├── js_wrapper.c │ ├── js_wrapper.h │ ├── meta_class.c │ ├── meta_class.h │ ├── plugin_loader.c │ ├── plugin_loader.h │ ├── qml.c │ ├── qml.h │ ├── refcounter.cpp │ ├── refcounter.h │ ├── rubyqml-plugin │ ├── listmodel.cpp │ ├── listmodel.h │ ├── rubyqml-plugin.pro │ ├── rubyqmlplugin.cpp │ └── rubyqmlplugin.h │ ├── signal_emitter.c │ └── signal_emitter.h ├── lib ├── qml.rb └── qml │ ├── access.rb │ ├── application.rb │ ├── component.rb │ ├── core_ext.rb │ ├── core_ext │ └── to_qml.rb │ ├── data.rb │ ├── data │ ├── array_model.rb │ ├── list_model.rb │ ├── list_model_access.rb │ └── query_model.rb │ ├── engine.rb │ ├── errors.rb │ ├── interface.rb │ ├── js_array.rb │ ├── js_object.rb │ ├── js_util.rb │ ├── name_helper.rb │ ├── platform.rb │ ├── plugin_loader.rb │ ├── plugins.rb │ ├── proc_access.rb │ ├── qml.rb │ ├── qt.rb │ ├── reactive.rb │ ├── root_path.rb │ ├── signal.rb │ └── version.rb ├── qml.gemspec └── spec ├── assets ├── test.png ├── testmodule │ ├── qmldir │ └── test.qml └── testobj.qml ├── qml ├── access_spec.rb ├── application_spec.rb ├── component_spec.rb ├── data │ ├── array_model_spec.rb │ ├── list_model_spec.rb │ └── query_model_spec.rb ├── engine_spec.rb ├── interface_spec.rb ├── js_array_spec.rb ├── js_function_spec.rb ├── js_object_spec.rb ├── plugin_loader_spec.rb ├── qt_spec.rb ├── reactive_spec.rb ├── signal_connect_spec.rb ├── signal_spec.rb └── to_qml_spec.rb ├── qml_spec.rb ├── shared └── qml │ ├── access_example.rb │ └── data │ └── list_model.rb └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | Gemfile.lock 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | doc/ 11 | lib/bundler/man 12 | pkg 13 | rdoc 14 | spec/reports 15 | test/tmp 16 | test/version_tmp 17 | tmp 18 | *.log 19 | .ruby-version 20 | 21 | # C++ objects and libs 22 | 23 | *.slo 24 | *.lo 25 | *.o 26 | *.a 27 | *.la 28 | *.lai 29 | *.so 30 | *.dll 31 | *.dylib 32 | *.bundle 33 | 34 | # Qt-es 35 | 36 | *.pro.user 37 | *.pro.user.* 38 | *.moc 39 | moc_*.cpp 40 | qrc_*.cpp 41 | Makefile 42 | *-build-* 43 | .qmake.stash 44 | *.autosave 45 | 46 | examples/twitter/config.yml 47 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "ext/qml/lib/libqmlbind"] 2 | path = ext/qml/lib/libqmlbind 3 | url = https://github.com/seanchas116/libqmlbind.git 4 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | dist: trusty 3 | language: ruby 4 | rvm: 5 | - 2.2 6 | - 2.1 7 | before_install: 8 | - sudo apt-add-repository -y ppa:beineri/opt-qt56-trusty 9 | - sudo apt-get update 10 | - sudo apt-get install -y qt56base qt56declarative 11 | - gem update --system 12 | - gem update bundler 13 | before_script: 14 | - source /opt/qt56/bin/qt56-env.sh 15 | - cd ext/qml 16 | - bundle exec ruby extconf.rb 17 | - make 18 | -------------------------------------------------------------------------------- /.yardopts: -------------------------------------------------------------------------------- 1 | --exclude lib/qml/qt_classes.rb 2 | --protected 3 | --no-private 4 | 5 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in qml.gemspec 4 | gemspec name: "qml" 5 | 6 | gem 'coveralls', require: false 7 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014-2015 Ryohei Ikegami 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ruby-qml [![Gem Version](https://badge.fury.io/rb/qml.svg)](http://badge.fury.io/rb/qml) 2 | ================ 3 | 4 | ruby-qml is a QML / Qt Quick wrapper for Ruby. 5 | It provides bindings between QML and Ruby and enables you to use Qt Quick-based GUI from Ruby. 6 | 7 | * [Documentation](http://rubydoc.info/github/seanchas116/ruby-qml/master/frames) 8 | * [Examples](https://github.com/seanchas116/ruby-qml/tree/master/examples) 9 | * [Changelog](https://github.com/seanchas116/ruby-qml/blob/master/changes.md) 10 | 11 | [![Dependency Status](https://gemnasium.com/seanchas116/ruby-qml.svg)](https://gemnasium.com/seanchas116/ruby-qml) 12 | [![Build Status](https://travis-ci.org/seanchas116/ruby-qml.svg?branch=master)](https://travis-ci.org/seanchas116/ruby-qml) 13 | [![Coverage Status](https://coveralls.io/repos/seanchas116/ruby-qml/badge.png?branch=master)](https://coveralls.io/r/seanchas116/ruby-qml?branch=master) 14 | [![Inline docs](http://inch-ci.org/github/seanchas116/ruby-qml.png?branch=master)](http://inch-ci.org/github/seanchas116/ruby-qml) 15 | 16 | ## What you can do with ruby-qml 17 | 18 | * Develop desktop GUI applications only with Ruby and QML / JavaScript 19 | * Easily combine codes written in C++ and Qt with your Ruby code 20 | 21 | ## Gallery 22 | 23 | [![Screenshot](https://raw.github.com/seanchas116/ruby-qml/master/examples/todo_sequel/capture.png)](https://github.com/seanchas116/ruby-qml/tree/master/examples/todo_sequel) 24 | 25 | [![Screenshot](https://raw.github.com/seanchas116/ruby-qml/master/examples/twitter/capture.png)](https://github.com/seanchas116/ruby-qml/tree/master/examples/twitter) 26 | 27 | ## Installation 28 | 29 | ### Requirements 30 | 31 | * **Ruby 2.1 or later** 32 | * **OS X or Linux** 33 | * Qt 5.4 or later 34 | 35 | ### OS X with Homebrew 36 | 37 | To install ruby-qml on OS X with Homebrew, run the following commands: 38 | 39 | $ brew install qt5 40 | $ gem install qml -- --with-qmake=$(brew --prefix qt5)/bin/qmake 41 | 42 | Both libffi and Qt5 are keg-only in Homebrew, so you must specify their paths explicitly (or force linking). 43 | 44 | If you use [official Qt installation](http://qt-project.org/downloads), for example: 45 | 46 | $ gem install qml -- --with-qmake=$HOME/Qt/5.4/clang_64/bin/qmake 47 | 48 | The Qt installation path (`$HOME/Qt/5.4/clang_64` in this example) depends on your Qt installation configuration and Qt version. 49 | 50 | ### General (OSX and Linux) 51 | 52 | $ gem install qml 53 | 54 | ### Ubuntu 55 | 56 | $ sudo apt install ruby ruby-dev build-essentials qt5-default qtdeclarative5-dev qtbase5-private-dev qml-module-qtquick2 qml-module-qtquick-controls 57 | $ sudo gem install qml 58 | 59 | ### WSL 60 | 61 | Using Ubuntu as the linux distro, proceed as above and use either WSL2 or an XServer (e.g. vcxsrv) to show the UI on Windows. 62 | 63 | #### Options 64 | 65 | * `--with-qmake=[dir]` 66 | * Qt qmake executable path (optional). 67 | 68 | ### Use Gemfile 69 | 70 | Add this line to your Gemfile: 71 | 72 | gem 'qml' 73 | 74 | And then execute: 75 | 76 | $ bundle install 77 | 78 | To pass build options, use `bundle config`. 79 | For example: 80 | 81 | $ bundle config build.qml --with-qmake=$(brew --prefix qt5)/bin/qmake 82 | 83 | The configuration will be saved in `~/.bundle/config`. 84 | 85 | ## Usage 86 | 87 | ### Load QML file 88 | 89 | The following code loads a QML file and shows an application window titled "Hello, world!". 90 | 91 | ```ruby 92 | require 'qml' 93 | 94 | QML.run do |app| 95 | app.load_path Pathname(__FILE__) + '../main.qml' 96 | end 97 | ``` 98 | 99 | ```qml 100 | // main.qml 101 | import QtQuick 2.2 102 | import QtQuick.Controls 1.1 103 | 104 | ApplicationWindow { 105 | visible: true 106 | width: 200 107 | height: 100 108 | title: "Hello, world!" 109 | } 110 | ``` 111 | 112 | ### Use Ruby class in QML 113 | 114 | To make your class available to QML, include `QML::Access` and call `register_to_qml`. 115 | 116 | By including `QML::Access`, you can also define **properties and signals** in Ruby classes like in QML. 117 | 118 | Properties are used to bind data between QML and Ruby. 119 | Signals are used to provide the observer pattern-like notification from Ruby to QML. 120 | 121 | ![Screenshot](https://raw.github.com/seanchas116/ruby-qml/master/examples/fizzbuzz/capture.png) 122 | 123 | ```ruby 124 | # Ruby 125 | class FizzBuzz 126 | include QML::Access 127 | register_to_qml under: "Example", version: "1.0" 128 | 129 | property(:input) { '0' } 130 | property(:result) { '' } 131 | signal :inputWasFizzBuzz, [] 132 | 133 | on_changed :input do 134 | i = input.to_i 135 | self.result = case 136 | when i % 15 == 0 137 | inputWasFizzBuzz.emit 138 | "FizzBuzz" 139 | when i % 3 == 0 140 | "Fizz" 141 | when i % 5 == 0 142 | "Buzz" 143 | else 144 | i.to_s 145 | end 146 | end 147 | 148 | def quit 149 | puts "quitting..." 150 | QML.application.quit 151 | end 152 | end 153 | ``` 154 | 155 | ```qml 156 | // QML - main.qml 157 | import QtQuick 2.2 158 | import QtQuick.Controls 1.1 159 | import QtQuick.Layouts 1.1 160 | import Example 1.0 161 | 162 | ApplicationWindow { 163 | visible: true 164 | width: 200 165 | height: 200 166 | title: "FizzBuzz" 167 | 168 | ColumnLayout { 169 | anchors.fill: parent 170 | anchors.margins: 10 171 | TextField { 172 | placeholderText: "Input" 173 | text: "0" 174 | id: textField 175 | } 176 | Text { 177 | id: text 178 | text: fizzBuzz.result 179 | } 180 | Button { 181 | text: 'Quit' 182 | onClicked: fizzBuzz.quit() 183 | } 184 | Text { 185 | id: lastFizzBuzz 186 | } 187 | } 188 | FizzBuzz { 189 | id: fizzBuzz 190 | input: textField.text 191 | onInputWasFizzBuzz: lastFizzBuzz.text = "Last FizzBuzz: " + textField.text 192 | } 193 | } 194 | ``` 195 | 196 | You can omit arguments of `register_to_qml` if they are obvious: 197 | 198 | ```ruby 199 | module Example 200 | VERSION = '1.0.0' 201 | 202 | class FizzBuzz 203 | include QML::Access 204 | register_to_qml 205 | 206 | ... 207 | end 208 | end 209 | ``` 210 | 211 | ### Pass data to QML ListModels 212 | 213 | To bind list data between QML ListView and Ruby, you can use ListModels. 214 | 215 | * `QML::ListModel` - the base class for ruby-qml list models. 216 | 217 | * `QML::ArrayModel` - provides a simple list model implementation using Array. 218 | 219 | * `QML::QueryModel` - for databases (like ActiveRecord, Sequel or something) 220 | 221 | This example uses `ArrayModel` to provide list data for a QML ListView. 222 | When the content of the ArrayModel is changed, the list view is also automatically updated. 223 | 224 | #### Examples 225 | 226 | * [Todo example](https://github.com/seanchas116/ruby-qml/tree/master/examples/todo_sequel) 227 | 228 | ```ruby 229 | # Ruby 230 | class TodoController 231 | include QML::Access 232 | register_to_qml under: "Example", version: "1.0" 233 | 234 | property(:model) { QML::ArrayModel.new(:title, :description, :due_date) } 235 | 236 | def add(title, description, due_date) 237 | # Items of list models must be "Hash-like" (have #[] method to get columns) 238 | item = { 239 | title: title, 240 | description: description, 241 | due_date: due_date 242 | } 243 | model << item 244 | end 245 | end 246 | ``` 247 | 248 | ```qml 249 | // QML 250 | ListView { 251 | model: todo.model 252 | delegate: Text { 253 | text: "Title: " + title + ", Description: " + description + ", Due date: " + due_date 254 | } 255 | } 256 | TodoController { 257 | id: todo 258 | } 259 | ``` 260 | 261 | ### Combile asynchronous operations 262 | 263 | In QML, all UI-related operations are done synchronously in the event loop. 264 | To set result of asynchronous operations to the UI, use `QML.next_tick`. 265 | 266 | #### Examples 267 | 268 | * [Twitter Example](https://github.com/seanchas116/ruby-qml/tree/master/examples/twitter) 269 | 270 | ```ruby 271 | # Ruby 272 | class HeavyTaskController 273 | include QML::Access 274 | register_to_qml under: "Example", version: "1.0" 275 | 276 | property(:result) { '' } 277 | 278 | def set_result(result) 279 | self.result = result 280 | end 281 | 282 | def start_heavy_task 283 | Thread.new do 284 | QML.next_tick do 285 | set_result do_heavy_task() 286 | end 287 | end 288 | end 289 | end 290 | ``` 291 | 292 | ```qml 293 | // QML 294 | Text { 295 | text: controller.result 296 | } 297 | Button { 298 | text: "Start!!" 299 | onClicked: controller.start_heavy_task() 300 | } 301 | HeavyTaskController { 302 | id: controller 303 | } 304 | ``` 305 | 306 | ### Value conversions between Ruby and QML JavaScript 307 | 308 | 309 | #### Ruby to QML 310 | 311 | |Ruby |QML/JavaScript | 312 | |----------------|--------------------------------| 313 | |nil |null | 314 | |true/false |boolean | 315 | |Numeric |number | 316 | |String/Symbol |string | 317 | |Array |Array | 318 | |Hash |plain Object | 319 | |Proc |Function | 320 | |Time |Date | 321 | |QML::Access |Object(QObject derived) | 322 | |QML::ListModel |Object(QAbstractListModel) | 323 | 324 | You can customize this by implementing `#to_qml` method. 325 | 326 | #### QML to Ruby 327 | 328 | |QML/JavaScript |Ruby | 329 | |--------------------------------|----------------| 330 | |null/undefined |nil | 331 | |boolean |true/false | 332 | |number |Float | 333 | |string |String | 334 | |Array |QML::JSArray | 335 | |Function |QML::JSFunction | 336 | |Object |QML::JSObject | 337 | |Object wrapping QML::Access |QML::JSWrapper | 338 | 339 | You can convert Objects further through QML::JSObject methods. 340 | 341 | 342 | ### QML::JSObject usage 343 | 344 | `QML::JSObject` is the wrapper class for JavaScript objects. 345 | 346 | ```ruby 347 | obj = QML.engine.evaluate <<-JS 348 | ({ 349 | value: 1, 350 | add: function(d) { 351 | this.value += d; 352 | } 353 | }) 354 | JS 355 | 356 | # Getter 357 | obj.value #=> 1 358 | 359 | # Setter 360 | obj.value = 2 361 | obj.vaue #=> 2 362 | 363 | # Call method if the property is a function 364 | obj.add(10) 365 | obj.value #=> 11 366 | 367 | # Subscription 368 | obj[:value] #=> 11 369 | obj[:add] #=> # 370 | ``` 371 | 372 | ### Load and use Qt C++ plugins 373 | 374 | `PluginLoader` loads Qt C++ plugins. 375 | It enables you to use your Qt C++ codes from Ruby easily. 376 | 377 | ```c++ 378 | // C++ - plugin example 379 | class MyPlugin : public QObject 380 | { 381 | Q_OBJECT 382 | Q_PLUGIN_METADATA(IID "org.myplugin.MyPlugin") 383 | signals: 384 | void added(int value); 385 | 386 | public slots: 387 | int add(int x, int y) { 388 | int result = x + y; 389 | emit added(result); 390 | return result; 391 | } 392 | }; 393 | ``` 394 | 395 | ```ruby 396 | # Ruby 397 | 398 | # The instance will be a `QML::JSObject` which represents the plugin Qt object 399 | plugin = QML::PluginLoader.new(directory, "myplugin").instance 400 | 401 | # Connect to signal (see http://doc.qt.io/qt-5/qtqml-syntax-signals.html#connecting-signals-to-methods-and-signals) 402 | plugin[:added].connect do |value| 403 | puts "added value: #{value}" 404 | end 405 | 406 | plugin.add(1, 2) #=> 3 407 | ``` 408 | 409 | ### Use with EventMachine 410 | 411 | You can use [EventMachine](https://github.com/eventmachine/eventmachine) with ruby-qml. 412 | It is more powerful than the default ruby-qml event loop. 413 | 414 | Instead of using `QML.run`, start an EventMachine event loop by `EM.run` and 415 | process QML events periodically by `QML::Application#process_events`. 416 | 417 | ```ruby 418 | require 'qml' 419 | require 'eventmachine' 420 | 421 | EM.run do 422 | QML.init 423 | EM.add_periodic_timer(0.01) { QML.application.process_events } 424 | QML.application.load_path(Pathname.pwd + 'main.qml') 425 | end 426 | ``` 427 | 428 | You can also use [em-synchrony](https://github.com/igrigorik/em-synchrony) to 429 | write callback-free asynchronous operation for ruby-qml. 430 | 431 | ```ruby 432 | require 'qml' 433 | require 'eventmachine' 434 | require 'em-synchrony' 435 | require 'em-http-request' 436 | 437 | class Controller 438 | include QML::Access 439 | property(:result) { '' } 440 | 441 | def get 442 | EM.synchrony do 443 | content = EM::Synchrony.sync EM::HttpRequest.new('http://www.example.com/').get 444 | self.result = content.response 445 | end 446 | end 447 | 448 | def quit 449 | EM.stop 450 | end 451 | 452 | register_to_qml under: 'Example', version: '0.1' 453 | end 454 | 455 | EM.run do 456 | QML.init 457 | EM.add_periodic_timer(0.01) { QML.application.process_events } 458 | QML.application.load_path(Pathname.pwd + 'main.qml') 459 | end 460 | ``` 461 | 462 | ## Contributing 463 | 464 | ### Init submodules 465 | 466 | ``` 467 | $ git submodule init 468 | $ git submodule update 469 | ``` 470 | 471 | ### Install dependencies 472 | 473 | ``` 474 | $ bundle install 475 | ``` 476 | 477 | ### Build native extension 478 | 479 | Before running ruby-qml in development, the native extension of ruby-qml needs to have been built. 480 | To build it, run the following commands: 481 | 482 | ``` 483 | $ cd ext/qml 484 | $ bundle exec ruby extconf.rb --with-qmake=/path/to/qmake 485 | $ make -j4 486 | ``` 487 | 488 | ### Run tests 489 | 490 | Tests for ruby-qml is written in RSpec. To run tests, do: 491 | 492 | ``` 493 | $ bundle exec rspec 494 | ``` 495 | 496 | ### Run examples 497 | 498 | ``` 499 | $ bundle exec ruby examples/fizzbuzz/fizzbuzz.rb 500 | ``` 501 | 502 | ### Send pull requests 503 | 504 | 1. Fork it ( http://github.com/seanchas116/ruby-qml/fork ) 505 | 2. Create your feature branch (`git checkout -b my-new-feature`) 506 | 3. Commit your changes (`git commit -am 'Add some feature'`) 507 | 4. Write some tests 508 | 5. Push to the branch (`git push origin my-new-feature`) 509 | 6. Create new Pull Request 510 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rspec/core/rake_task" 3 | 4 | RSpec::Core::RakeTask.new(:spec) 5 | 6 | task :default => :spec 7 | -------------------------------------------------------------------------------- /changes.md: -------------------------------------------------------------------------------- 1 | ## 1.0.2 (2015-07-11) 2 | 3 | * Fixes error when calling `next_tick` from Ruby threads (#24) 4 | 5 | ## 1.0.1 (2015-06-20) 6 | 7 | * Fixes 100% CPU usage (#21) 8 | 9 | ## 1.0.0 (2015-06-16) 10 | 11 | * A lot of changes (see examples and docs) 12 | * Now uses [libqmlbind](https://github.com/seanchas116/libqmlbind) 13 | 14 | ## 0.0.7 (2014-12-25) 15 | 16 | * Support Ruby 2.2 (rc1) and Qt 5.4 17 | * Add `Engine#add_import_path` and `Engine#import_paths` by @tokoro10g 18 | 19 | ## 0.0.6 (2014-08-19) 20 | 21 | * Fix problems when using Fiber with ruby-qml 22 | 23 | * Rename block-receiving `QML.application` to `QML.run` 24 | 25 | * `QML.application` without block is still the same name 26 | 27 | ## 0.0.5 (2014-07-31) 28 | 29 | * Support official Qt installation on Mac 30 | 31 | ## 0.0.4 (2014-07-24) 32 | 33 | * Fix 100% CPU usage in idling 34 | 35 | ## 0.0.3 (2014-07-22) 36 | 37 | * Support Ruby 1.9.3 38 | 39 | ## 0.0.2 (2014-07-21) 40 | 41 | * Improve list models 42 | 43 | * Add ArrayModel#replace 44 | * Add QueryModel for ORMs 45 | * Specify column names in ListModel#initialize 46 | 47 | * Add To-do example with [Sequel](http://sequel.jeremyevans.net/) 48 | 49 | * Bug fixes 50 | 51 | * Improve README 52 | 53 | ## 0.0.1 (2014-07-19) 54 | 55 | * Initial release 56 | -------------------------------------------------------------------------------- /examples/assets/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seanchas116/ruby-qml/569af4c31460d2946c18346dc94751c77b1d4dfc/examples/assets/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /examples/fizzbuzz/capture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seanchas116/ruby-qml/569af4c31460d2946c18346dc94751c77b1d4dfc/examples/fizzbuzz/capture.png -------------------------------------------------------------------------------- /examples/fizzbuzz/fizzbuzz.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift File.expand_path('../../../lib', __FILE__) 2 | require 'qml' 3 | require 'pathname' 4 | 5 | module Examples 6 | module FizzBuzz 7 | VERSION = '0.1' 8 | 9 | class FizzBuzz 10 | include QML::Access 11 | register_to_qml 12 | 13 | property(:input) { '0' } 14 | property(:result) { '' } 15 | signal :inputWasFizzBuzz, [] 16 | 17 | on_changed :input do 18 | i = input.to_i 19 | self.result = case 20 | when i % 15 == 0 21 | inputWasFizzBuzz.emit 22 | "FizzBuzz" 23 | when i % 3 == 0 24 | "Fizz" 25 | when i % 5 == 0 26 | "Buzz" 27 | else 28 | i.to_s 29 | end 30 | end 31 | 32 | def quit 33 | puts "quitting..." 34 | QML.application.quit 35 | end 36 | end 37 | end 38 | end 39 | 40 | QML.run do |app| 41 | app.load_path Pathname(__FILE__) + '../main.qml' 42 | end 43 | -------------------------------------------------------------------------------- /examples/fizzbuzz/main.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.2 2 | import QtQuick.Controls 1.1 3 | import QtQuick.Layouts 1.1 4 | import Examples.FizzBuzz 0.1 5 | 6 | ApplicationWindow { 7 | visible: true 8 | width: 200 9 | height: 200 10 | title: "FizzBuzz" 11 | 12 | ColumnLayout { 13 | anchors.fill: parent 14 | anchors.margins: 10 15 | TextField { 16 | placeholderText: "Input" 17 | text: "0" 18 | id: textField 19 | } 20 | Text { 21 | id: text 22 | text: fizzBuzz.result 23 | } 24 | Button { 25 | text: 'Quit' 26 | onClicked: fizzBuzz.quit() 27 | } 28 | Text { 29 | id: lastFizzBuzz 30 | } 31 | } 32 | FizzBuzz { 33 | id: fizzBuzz 34 | input: textField.text 35 | onInputWasFizzBuzz: lastFizzBuzz.text = "Last FizzBuzz: " + textField.text 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /examples/todo_array/capture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seanchas116/ruby-qml/569af4c31460d2946c18346dc94751c77b1d4dfc/examples/todo_array/capture.png -------------------------------------------------------------------------------- /examples/todo_array/main.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.3 2 | import QtQuick.Controls 1.2 3 | import QtQuick.Layouts 1.1 4 | import Examples.Todo 0.1 5 | 6 | ApplicationWindow { 7 | visible: true 8 | title: "Todo Array Model" 9 | 10 | FontLoader { 11 | source: "../assets/fonts/fontawesome-webfont.ttf" 12 | } 13 | width: layout.implicitWidth 14 | height: layout.implicitHeight 15 | 16 | RowLayout { 17 | id: layout 18 | anchors.fill: parent 19 | anchors.margins: 10 20 | ColumnLayout { 21 | TextField { 22 | placeholderText: "Title" 23 | id: titleField 24 | } 25 | TextField { 26 | placeholderText: "Description" 27 | id: descriptionField 28 | } 29 | Calendar { 30 | id: calendar 31 | } 32 | Button { 33 | text: "Add" 34 | onClicked: todo.add() 35 | } 36 | } 37 | ListView { 38 | model: todo.model 39 | spacing: 10 40 | Layout.fillWidth: true 41 | Layout.fillHeight: true 42 | Layout.alignment: Qt.AlignTop 43 | Layout.minimumWidth: 300 44 | delegate: ColumnLayout { 45 | Text { 46 | font.bold: true 47 | text: title 48 | } 49 | Text { 50 | text: description 51 | } 52 | RowLayout { 53 | Text { 54 | font.family: "FontAwesome" 55 | text: "\uf073" 56 | } 57 | Text { 58 | text: Qt.formatDateTime(due_date) 59 | } 60 | } 61 | } 62 | } 63 | } 64 | TodoController { 65 | id: todo 66 | title: titleField.text 67 | description: descriptionField.text 68 | due_date: calendar.selectedDate 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /examples/todo_array/todo_array.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift File.expand_path('../../../lib', __FILE__) 2 | require 'qml' 3 | require 'pathname' 4 | 5 | module Examples 6 | module Todo 7 | VERSION = '0.1' 8 | 9 | class TodoController 10 | include QML::Access 11 | register_to_qml 12 | 13 | property(:title) { '' } 14 | property(:description) { '' } 15 | property(:due_date) { '' } 16 | property(:model) { QML::ArrayModel.new(:title, :description, :due_date) } 17 | 18 | def add 19 | item = { 20 | title: title, 21 | description: description, 22 | due_date: due_date 23 | } 24 | p item 25 | model << item 26 | end 27 | end 28 | end 29 | end 30 | 31 | QML.run do |app| 32 | app.load_path Pathname(__FILE__) + '../main.qml' 33 | end 34 | -------------------------------------------------------------------------------- /examples/todo_sequel/capture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seanchas116/ruby-qml/569af4c31460d2946c18346dc94751c77b1d4dfc/examples/todo_sequel/capture.png -------------------------------------------------------------------------------- /examples/todo_sequel/main.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.3 2 | import QtQuick.Controls 1.2 3 | import QtQuick.Layouts 1.1 4 | import Examples.Todo 0.1 5 | 6 | ApplicationWindow { 7 | visible: true 8 | title: "Todo with Sequel" 9 | 10 | FontLoader { 11 | source: "../assets/fonts/fontawesome-webfont.ttf" 12 | } 13 | id: window 14 | property int margin: 10 15 | width: layout.implicitWidth + 2 * margin 16 | height: layout.implicitHeight + 2 * margin 17 | 18 | RowLayout { 19 | id: layout 20 | anchors.fill: parent 21 | anchors.margins: window.margin 22 | ColumnLayout { 23 | TextField { 24 | placeholderText: "Title" 25 | id: titleField 26 | } 27 | TextField { 28 | placeholderText: "Description" 29 | id: descriptionField 30 | } 31 | Calendar { 32 | id: calendar 33 | } 34 | Button { 35 | text: "Add" 36 | onClicked: todo.add() 37 | } 38 | } 39 | ColumnLayout { 40 | RowLayout { 41 | Label { text: "Sort by" } 42 | ComboBox { 43 | id: orderComboBox 44 | model: ListModel { 45 | ListElement { text: "Title"; column: "title" } 46 | ListElement { text: "Description"; column: "description" } 47 | ListElement { text: "Due Date"; column: "due_date" } 48 | } 49 | property string currentColumn: model.get(currentIndex).column 50 | } 51 | } 52 | ListView { 53 | model: todo.model 54 | spacing: 10 55 | Layout.fillWidth: true 56 | Layout.fillHeight: true 57 | Layout.alignment: Qt.AlignTop 58 | Layout.minimumWidth: 300 59 | delegate: ColumnLayout { 60 | Text { 61 | font.bold: true 62 | text: title 63 | } 64 | Text { 65 | text: description 66 | } 67 | RowLayout { 68 | Text { 69 | font.family: "FontAwesome" 70 | text: "\uf073" 71 | } 72 | Text { 73 | text: Qt.formatDate(due_date) 74 | } 75 | } 76 | } 77 | } 78 | } 79 | } 80 | TodoController { 81 | id: todo 82 | title: titleField.text 83 | description: descriptionField.text 84 | due_date: calendar.selectedDate 85 | order_by: orderComboBox.currentColumn 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /examples/todo_sequel/todo_sequel.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift File.expand_path('../../../lib', __FILE__) 2 | require 'qml' 3 | require 'sequel' 4 | 5 | module Examples 6 | module Todo 7 | VERSION = '0.1' 8 | 9 | DB = Sequel.sqlite 10 | 11 | DB.create_table :todos do 12 | primary_key :id 13 | String :title 14 | String :description 15 | Time :due_date 16 | end 17 | 18 | class SequelModel < QML::QueryModel 19 | attr_accessor :dataset 20 | 21 | def initialize(dataset) 22 | @dataset = dataset 23 | super(*dataset.columns) 24 | end 25 | 26 | def query_count 27 | @dataset.count 28 | end 29 | 30 | def query(offset, count) 31 | @dataset.offset(offset).limit(count).all 32 | end 33 | end 34 | 35 | class TodoController 36 | include QML::Access 37 | register_to_qml 38 | 39 | def initialize 40 | super 41 | @todo_dataset = DB[:todos] 42 | self.model = SequelModel.new(@todo_dataset) 43 | end 44 | 45 | property(:title) { '' } 46 | property(:description) { '' } 47 | property(:due_date) { '' } 48 | property(:order_by) { '' } 49 | property :model 50 | 51 | def add 52 | @todo_dataset.insert(title: title, description: description, due_date: due_date.to_time) 53 | model.update 54 | end 55 | 56 | on_changed :order_by do 57 | model.dataset = @todo_dataset.order(order_by.to_sym) 58 | model.update 59 | end 60 | end 61 | end 62 | end 63 | 64 | QML.run do |app| 65 | app.load_path Pathname(__FILE__) + '../main.qml' 66 | end 67 | -------------------------------------------------------------------------------- /examples/twitter/capture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seanchas116/ruby-qml/569af4c31460d2946c18346dc94751c77b1d4dfc/examples/twitter/capture.png -------------------------------------------------------------------------------- /examples/twitter/main.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.2 2 | import QtQuick.Controls 1.1 3 | import QtQuick.Layouts 1.1 4 | import Examples.Twitter 0.1 5 | 6 | ApplicationWindow { 7 | visible: true 8 | width: 300 9 | height: 500 10 | title: "Twitter Search Test - " + controller.word 11 | 12 | ColumnLayout { 13 | anchors.fill: parent 14 | anchors.margins: 10 15 | TextField { 16 | id: wordField 17 | placeholderText: 'Search...' 18 | onEditingFinished: controller.fetch_tweets() 19 | } 20 | ScrollView { 21 | Layout.fillHeight: true 22 | Layout.fillWidth: true 23 | ListView { 24 | model: controller.model 25 | delegate: RowLayout { 26 | Image { 27 | width: 100 28 | height: 100 29 | source: user_icon 30 | } 31 | ColumnLayout { 32 | Text { 33 | font.bold: true 34 | text: user_name 35 | } 36 | Text { 37 | text: tweet_text 38 | } 39 | } 40 | } 41 | } 42 | } 43 | } 44 | TwitterController { 45 | id: controller 46 | word: wordField.text 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /examples/twitter/twitter.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift File.expand_path('../../../lib', __FILE__) 2 | require 'qml' 3 | require 'twitter' 4 | require 'yaml' 5 | 6 | module Examples 7 | module Twitter 8 | VERSION = '0.1' 9 | 10 | class TweetFetcher 11 | def initialize 12 | config_data = YAML.load((Pathname(__FILE__) + '../config.yml').read) 13 | config_keys = %w{consumer_key consumer_secret access_token access_token_secret} 14 | 15 | @rest_client = ::Twitter::REST::Client.new do |config| 16 | config_keys.each do |key| 17 | config.public_send("#{key}=", config_data[key]) 18 | end 19 | end 20 | @streaming_client = ::Twitter::Streaming::Client.new do |config| 21 | config_keys.each do |key| 22 | config.public_send("#{key}=", config_data[key]) 23 | end 24 | end 25 | end 26 | 27 | def start(word) 28 | puts "word = #{word}" 29 | @rest_client.search(word).take(10).each do |t| 30 | yield t 31 | end 32 | @streaming_client.filter(track: word) do |object| 33 | case object 34 | when ::Twitter::Tweet 35 | yield object 36 | end 37 | end 38 | end 39 | end 40 | 41 | class TwitterController 42 | include QML::Access 43 | register_to_qml 44 | 45 | property(:model) { QML::ArrayModel.new(:tweet_text, :user_name, :user_icon) } 46 | property :word 47 | 48 | def initialize 49 | super() 50 | end 51 | 52 | def add_tweet(tweet) 53 | hash = {tweet_text: tweet.text, user_name: tweet.user.name, user_icon: tweet.user.profile_image_uri.to_s} 54 | puts hash 55 | model.unshift hash 56 | end 57 | 58 | def fetch_tweets 59 | model.clear 60 | if @thread 61 | @thread.kill 62 | end 63 | word = self.word 64 | @thread = Thread.new do 65 | TweetFetcher.new.start(word) do |tweet| 66 | QML.next_tick do 67 | add_tweet(tweet) 68 | end 69 | end 70 | end 71 | nil 72 | end 73 | end 74 | end 75 | end 76 | 77 | QML.run do |app| 78 | app.load_path Pathname(__FILE__) + '../main.qml' 79 | end 80 | -------------------------------------------------------------------------------- /ext/qml/application.c: -------------------------------------------------------------------------------- 1 | #include "application.h" 2 | 3 | VALUE rbqml_cApplication = Qnil; 4 | 5 | typedef struct { 6 | qmlbind_application *application; 7 | } application_t; 8 | 9 | static void application_free(void *p) { 10 | application_t *data = (application_t*)p; 11 | // application is never released 12 | // qmlbind_application_release(data->application); 13 | xfree(data); 14 | } 15 | 16 | static const rb_data_type_t data_type = { 17 | "QML::Application", 18 | { NULL, &application_free } 19 | }; 20 | 21 | qmlbind_application *rbqml_get_application(VALUE self) { 22 | application_t *data; 23 | TypedData_Get_Struct(self, application_t, &data_type, data); 24 | return data->application; 25 | } 26 | 27 | static VALUE application_alloc(VALUE klass) { 28 | application_t *data = ALLOC(application_t); 29 | data->application = NULL; 30 | return TypedData_Wrap_Struct(klass, &data_type, data); 31 | } 32 | 33 | static VALUE application_name(VALUE self, VALUE name) { 34 | application_t *data; 35 | 36 | Check_Type(name, T_STRING); 37 | TypedData_Get_Struct(self, application_t, &data_type, data); 38 | if (data->application) { 39 | qmlbind_application_setapplicationname(RSTRING_PTR(name)); 40 | } 41 | return self; 42 | } 43 | 44 | static VALUE application_organization(VALUE self, VALUE name) { 45 | application_t *data; 46 | 47 | Check_Type(name, T_STRING); 48 | TypedData_Get_Struct(self, application_t, &data_type, data); 49 | if (data->application) { 50 | qmlbind_application_setorganizationname(RSTRING_PTR(name)); 51 | } 52 | return self; 53 | } 54 | 55 | static VALUE application_domain(VALUE self, VALUE name) { 56 | application_t *data; 57 | 58 | Check_Type(name, T_STRING); 59 | TypedData_Get_Struct(self, application_t, &data_type, data); 60 | if (data->application) { 61 | qmlbind_application_setorganizationdomain(RSTRING_PTR(name)); 62 | } 63 | return self; 64 | } 65 | 66 | static VALUE application_seticon(VALUE self, VALUE filename) { 67 | application_t *data; 68 | 69 | Check_Type(filename, T_STRING); 70 | TypedData_Get_Struct(self, application_t, &data_type, data); 71 | if (data->application) { 72 | qmlbind_application_seticon(RSTRING_PTR(filename)); 73 | } 74 | return self; 75 | } 76 | 77 | 78 | static VALUE application_initialize(VALUE self, VALUE args) { 79 | if (rb_thread_main() != rb_thread_current()) { 80 | rb_raise(rb_eThreadError, "Initializing QML::Application outside the main thread"); 81 | } 82 | 83 | application_t *data; 84 | TypedData_Get_Struct(self, application_t, &data_type, data); 85 | 86 | if (rb_type(args) != T_ARRAY) { 87 | rb_raise(rb_eTypeError, "Expected Array"); 88 | } 89 | 90 | args = rb_ary_concat(rb_ary_new_from_args(1, rb_argv0), args); 91 | 92 | int argc = RARRAY_LEN(args); 93 | const char **argv = malloc(argc * sizeof(char *)); 94 | 95 | for (int i = 0; i < argc; ++i) { 96 | VALUE arg = RARRAY_AREF(args, i); 97 | argv[i] = rb_string_value_cstr(&arg); 98 | } 99 | 100 | data->application = qmlbind_application_new(argc, argv); 101 | 102 | return self; 103 | } 104 | 105 | /* 106 | * Starts the event loop of the application. 107 | * This method never returns until the application quits. 108 | */ 109 | static VALUE application_exec(VALUE self) { 110 | qmlbind_application *app = rbqml_get_application(self); 111 | int ret = (int)rb_thread_call_without_gvl((void *(*)(void *))&qmlbind_application_exec, app, RUBY_UBF_IO, NULL); 112 | return INT2NUM(ret); 113 | } 114 | 115 | /* 116 | * Processes queued events in the event loop manually. 117 | * This method is useful when you are combining an external event loop like EventMachine. 118 | */ 119 | static VALUE application_process_events(VALUE application) { 120 | rb_thread_call_without_gvl((void *(*)(void *))&qmlbind_process_events, NULL, RUBY_UBF_IO, NULL); 121 | return Qnil; 122 | } 123 | 124 | void rbqml_init_application(void) { 125 | VALUE mQML = rb_define_module("QML"); 126 | rbqml_cApplication = rb_define_class_under(mQML, "Application", rb_cObject); 127 | rb_define_alloc_func(rbqml_cApplication, application_alloc); 128 | 129 | rb_define_private_method(rbqml_cApplication, "initialize", application_initialize, 1); 130 | rb_define_method(rbqml_cApplication, "exec", application_exec, 0); 131 | rb_define_method(rbqml_cApplication, "process_events", application_process_events, 0); 132 | rb_define_method(rbqml_cApplication, "name=", application_name, 1); 133 | rb_define_method(rbqml_cApplication, "organization=", application_organization, 1); 134 | rb_define_method(rbqml_cApplication, "domain=", application_domain, 1); 135 | rb_define_method(rbqml_cApplication, "icon=", application_seticon, 1); 136 | } 137 | -------------------------------------------------------------------------------- /ext/qml/application.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "qml.h" 4 | 5 | extern VALUE rbqml_cApplication; 6 | 7 | qmlbind_application *rbqml_get_application(VALUE self); 8 | 9 | void rbqml_init_application(void); 10 | -------------------------------------------------------------------------------- /ext/qml/component.c: -------------------------------------------------------------------------------- 1 | #include "qml.h" 2 | #include "component.h" 3 | #include "engine.h" 4 | #include "conversion.h" 5 | 6 | VALUE rbqml_cComponent; 7 | 8 | typedef struct { 9 | qmlbind_component *component; 10 | } component_t; 11 | 12 | static void component_free(void *p) { 13 | component_t *data = (component_t *)p; 14 | rb_thread_call_without_gvl((void *(*)(void *))&qmlbind_component_release, data->component, RUBY_UBF_IO, NULL); 15 | xfree(data); 16 | } 17 | 18 | static const rb_data_type_t data_type = { 19 | "QML::Component", 20 | { NULL, &component_free } 21 | }; 22 | 23 | qmlbind_component *rbqml_get_component(VALUE self) { 24 | component_t *data; 25 | TypedData_Get_Struct(self, component_t, &data_type, data); 26 | return data->component; 27 | } 28 | 29 | static VALUE component_alloc(VALUE klass) { 30 | component_t *data = ALLOC(component_t); 31 | data->component = NULL; 32 | return TypedData_Wrap_Struct(klass, &data_type, data); 33 | } 34 | 35 | static VALUE component_initialize(VALUE self) { 36 | component_t *data; 37 | TypedData_Get_Struct(self, component_t, &data_type, data); 38 | data->component = qmlbind_component_new(rbqml_get_engine(rbqml_engine)); 39 | 40 | return self; 41 | } 42 | 43 | static VALUE component_load_path(VALUE self, VALUE path) { 44 | qmlbind_component *component = rbqml_get_component(self); 45 | qmlbind_component_load_path(component, rb_string_value_cstr(&path)); 46 | 47 | return self; 48 | } 49 | 50 | static VALUE component_load_data(VALUE self, VALUE data, VALUE path) { 51 | qmlbind_component *component = rbqml_get_component(self); 52 | qmlbind_component_set_data(component, rb_string_value_cstr(&data), rb_string_value_cstr(&path)); 53 | 54 | return self; 55 | } 56 | 57 | static VALUE component_error_string(VALUE self) { 58 | qmlbind_component *component = rbqml_get_component(self); 59 | qmlbind_string *error = qmlbind_component_get_error_string(component); 60 | if (error) { 61 | VALUE str = rb_enc_str_new(qmlbind_string_get_chars(error), qmlbind_string_get_length(error), rb_utf8_encoding()); 62 | qmlbind_string_release(error); 63 | return str; 64 | } else { 65 | return Qnil; 66 | } 67 | } 68 | 69 | static VALUE component_create(VALUE self) { 70 | component_t *data; 71 | TypedData_Get_Struct(self, component_t, &data_type, data); 72 | 73 | qmlbind_value *obj = rb_thread_call_without_gvl((void *(*)(void *))&qmlbind_component_create, data->component, RUBY_UBF_IO, NULL); 74 | VALUE result = rbqml_to_ruby(obj); 75 | qmlbind_value_release(obj); 76 | 77 | return result; 78 | } 79 | 80 | void rbqml_init_component() { 81 | rbqml_cComponent = rb_define_class_under(rb_path2class("QML"), "Component", rb_cObject); 82 | rb_define_alloc_func(rbqml_cComponent, &component_alloc); 83 | 84 | rb_define_private_method(rbqml_cComponent, "initialize_impl", &component_initialize, 0); 85 | rb_define_private_method(rbqml_cComponent, "load_path_impl", &component_load_path, 1); 86 | rb_define_private_method(rbqml_cComponent, "load_data_impl", &component_load_data, 2); 87 | rb_define_private_method(rbqml_cComponent, "error_string", &component_error_string, 0); 88 | rb_define_private_method(rbqml_cComponent, "create_impl", &component_create, 0); 89 | } 90 | -------------------------------------------------------------------------------- /ext/qml/component.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | extern VALUE rbqml_cComponent; 7 | 8 | qmlbind_component *rbqml_get_component(VALUE value); 9 | 10 | void rbqml_init_component(void); 11 | -------------------------------------------------------------------------------- /ext/qml/conversion.c: -------------------------------------------------------------------------------- 1 | #include "qml.h" 2 | #include "conversion.h" 3 | #include "js_object.h" 4 | #include "js_array.h" 5 | #include "js_function.h" 6 | #include "js_wrapper.h" 7 | 8 | VALUE rbqml_to_ruby(const qmlbind_value *value) 9 | { 10 | if (qmlbind_value_is_undefined(value) || qmlbind_value_is_null(value)) { 11 | return Qnil; 12 | } 13 | if (qmlbind_value_is_boolean(value)) { 14 | return qmlbind_value_get_boolean(value) ? Qtrue : Qfalse; 15 | } 16 | if (qmlbind_value_is_number(value)) { 17 | double num = qmlbind_value_get_number(value); 18 | return rb_float_new(num); 19 | } 20 | if (qmlbind_value_is_string(value)) { 21 | qmlbind_string *str = qmlbind_value_get_string(value); 22 | VALUE ruby_str = rb_enc_str_new(qmlbind_string_get_chars(str), qmlbind_string_get_length(str), rb_utf8_encoding()); 23 | qmlbind_string_release(str); 24 | return ruby_str; 25 | } 26 | 27 | VALUE klass; 28 | 29 | if (qmlbind_value_is_array(value)) { 30 | klass = rbqml_cJSArray; 31 | } else if (qmlbind_value_is_function(value)) { 32 | klass = rbqml_cJSFunction; 33 | } else if (qmlbind_value_is_wrapper(value)) { 34 | klass = rbqml_cJSWrapper; 35 | } else { 36 | klass = rbqml_cJSObject; 37 | } 38 | 39 | return rbqml_js_object_new(klass, qmlbind_value_clone(value)); 40 | } 41 | 42 | qmlbind_value *rbqml_to_qml(VALUE value) 43 | { 44 | value = rb_funcall(value, rb_intern("to_qml"), 0); 45 | 46 | switch (rb_type(value)) { 47 | case T_NIL: 48 | return qmlbind_value_new_null(); 49 | case T_TRUE: 50 | return qmlbind_value_new_boolean(true); 51 | case T_FALSE: 52 | return qmlbind_value_new_boolean(false); 53 | case T_FLOAT: 54 | return qmlbind_value_new_number(rb_float_value(value)); 55 | case T_STRING: 56 | return qmlbind_value_new_string(RSTRING_LEN(value), RSTRING_PTR(value)); 57 | default: 58 | break; 59 | } 60 | 61 | return rbqml_js_object_get(value); 62 | } 63 | -------------------------------------------------------------------------------- /ext/qml/conversion.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | VALUE rbqml_to_ruby(const qmlbind_value *value); 8 | qmlbind_value *rbqml_to_qml(VALUE value); 9 | -------------------------------------------------------------------------------- /ext/qml/engine.c: -------------------------------------------------------------------------------- 1 | #include "engine.h" 2 | #include "conversion.h" 3 | #include "application.h" 4 | #include "js_object.h" 5 | #include "js_array.h" 6 | 7 | VALUE rbqml_cEngine; 8 | 9 | typedef struct { 10 | qmlbind_engine *engine; 11 | } engine_t; 12 | 13 | static void engine_free(void *p) { 14 | engine_t *data = (engine_t *)p; 15 | 16 | //rb_thread_call_without_gvl((void *(*)(void *))&qmlbind_engine_release, data->engine, RUBY_UBF_IO, NULL); 17 | xfree(data); 18 | } 19 | 20 | static const rb_data_type_t data_type = { 21 | "QML::Engine", 22 | { NULL, &engine_free } 23 | }; 24 | 25 | qmlbind_engine *rbqml_get_engine(VALUE self) { 26 | engine_t *data; 27 | TypedData_Get_Struct(self, engine_t, &data_type, data); 28 | return data->engine; 29 | } 30 | 31 | static VALUE engine_alloc(VALUE klass) { 32 | engine_t *data = ALLOC(engine_t); 33 | data->engine = NULL; 34 | return TypedData_Wrap_Struct(klass, &data_type, data); 35 | } 36 | 37 | static VALUE engine_initialize(VALUE self) { 38 | engine_t *data; 39 | TypedData_Get_Struct(self, engine_t, &data_type, data); 40 | data->engine = qmlbind_engine_new(); 41 | return self; 42 | } 43 | 44 | /* 45 | * Adds a QML import path to the {Engine}. 46 | * @param path [String] 47 | * @see http://doc.qt.io/qt-5/qtqml-syntax-imports.html#qml-import-path 48 | */ 49 | static VALUE engine_add_import_path(VALUE self, VALUE path) { 50 | qmlbind_engine *engine = rbqml_get_engine(self); 51 | path = rb_funcall(path, rb_intern("to_s"), 0); 52 | qmlbind_engine_add_import_path(engine, rb_string_value_cstr(&path)); 53 | return self; 54 | } 55 | 56 | typedef struct { 57 | qmlbind_engine *engine; 58 | const char *str; 59 | const char *file; 60 | int lineNum; 61 | } evaluate_data; 62 | 63 | static void *evaluate_impl(void *p) { 64 | evaluate_data *data = p; 65 | return qmlbind_engine_eval(data->engine, data->str, data->file, data->lineNum); 66 | } 67 | 68 | static VALUE engine_evaluate(VALUE self, VALUE str, VALUE file, VALUE lineNum) { 69 | qmlbind_engine *engine = rbqml_get_engine(self); 70 | 71 | evaluate_data data; 72 | data.engine = engine; 73 | data.str = rb_string_value_cstr(&str); 74 | data.file = rb_string_value_cstr(&file); 75 | data.lineNum = NUM2INT(lineNum); 76 | 77 | qmlbind_value *value = rb_thread_call_without_gvl(&evaluate_impl, &data, RUBY_UBF_IO, NULL); 78 | 79 | VALUE result = rbqml_to_ruby(value); 80 | qmlbind_value_release(value); 81 | 82 | return result; 83 | } 84 | 85 | /* 86 | * @paran [Integer] len 87 | * @return [QML::JSArray] 88 | */ 89 | static VALUE engine_new_array(VALUE self, VALUE len) { 90 | qmlbind_engine *engine = rbqml_get_engine(self); 91 | 92 | qmlbind_value *array = qmlbind_engine_new_array(engine, NUM2INT(len)); 93 | VALUE value = rbqml_js_object_new(rbqml_cJSArray, array); 94 | qmlbind_value_release(array); 95 | 96 | return value; 97 | } 98 | 99 | /* 100 | * @return [QML::JSObject] 101 | */ 102 | static VALUE engine_new_object(VALUE self) { 103 | qmlbind_engine *engine = rbqml_get_engine(self); 104 | 105 | qmlbind_value *obj = qmlbind_engine_new_object(engine); 106 | VALUE value = rbqml_js_object_new(rbqml_cJSObject, obj); 107 | qmlbind_value_release(obj); 108 | 109 | return value; 110 | } 111 | 112 | /* 113 | * Starts garbage collection on the {Engine}. 114 | */ 115 | static VALUE engine_collect_garbage(VALUE self) { 116 | qmlbind_engine *engine = rbqml_get_engine(self); 117 | qmlbind_engine_collect_garbage(engine); 118 | return self; 119 | } 120 | 121 | void rbqml_init_engine() { 122 | rb_require("qml/errors"); 123 | 124 | VALUE mQML = rb_define_module("QML"); 125 | 126 | rbqml_cEngine = rb_define_class_under(mQML, "Engine", rb_cObject); 127 | rb_define_alloc_func(rbqml_cEngine, &engine_alloc); 128 | 129 | rb_define_private_method(rbqml_cEngine, "initialize", engine_initialize, 0); 130 | rb_define_method(rbqml_cEngine, "add_import_path", engine_add_import_path, 1); 131 | rb_define_private_method(rbqml_cEngine, "evaluate_impl", engine_evaluate, 3); 132 | rb_define_method(rbqml_cEngine, "new_array", engine_new_array, 1); 133 | rb_define_method(rbqml_cEngine, "new_object", engine_new_object, 0); 134 | rb_define_method(rbqml_cEngine, "collect_garbage", engine_collect_garbage, 0); 135 | } 136 | -------------------------------------------------------------------------------- /ext/qml/engine.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | extern VALUE rbqml_cEngine; 7 | 8 | qmlbind_engine *rbqml_get_engine(VALUE value); 9 | 10 | void rbqml_init_engine(void); 11 | -------------------------------------------------------------------------------- /ext/qml/extconf.rb: -------------------------------------------------------------------------------- 1 | require 'mkmf' 2 | require 'pathname' 3 | require '../../lib/qml/platform' 4 | 5 | # find qmake 6 | 7 | qmake = with_config('qmake') || find_executable('qmake') 8 | debug_enabled = enable_config('debug') 9 | clean_enabled = enable_config('clean') 10 | qmake_opts = debug_enabled ? 'CONFIG+=debug' : '' 11 | 12 | # build libqmlbind 13 | 14 | qmlbind_dir = Pathname.pwd + 'lib/libqmlbind/qmlbind' 15 | 16 | Dir.chdir(qmlbind_dir) do 17 | puts " >>> building libqmlbind..." 18 | system "#{qmake} #{qmake_opts}" 19 | system "make clean" if clean_enabled 20 | system "make -j4" or abort "ERROR: Failed to build libqmlbind" 21 | end 22 | 23 | case 24 | when QML::Platform.mac? 25 | lib_path = qmlbind_dir + 'libqmlbind.dylib' 26 | system "install_name_tool -id #{lib_path} #{lib_path}" 27 | when QML::Platform.windows? 28 | # TODO 29 | else 30 | $LDFLAGS << " -Wl,-rpath #{qmlbind_dir}" 31 | end 32 | 33 | # build plugin 34 | 35 | Dir.chdir "rubyqml-plugin" do 36 | puts " >>> building rubyqml-plugin..." 37 | system "#{qmake} #{qmake_opts}" 38 | system "make clean" if clean_enabled 39 | system "make -j4" or abort "ERROR: Failed to build plugin" 40 | end 41 | 42 | puts " >>> configuring..." 43 | 44 | # create makefile 45 | 46 | $LDFLAGS << " -L#{qmlbind_dir} -lqmlbind" 47 | $CPPFLAGS << " -I#{qmlbind_dir + 'include'}" 48 | 49 | $CPPFLAGS << " -g" if debug_enabled 50 | 51 | $CFLAGS << " -std=c99" 52 | $CXXFLAGS << " -std=c++11" 53 | 54 | # create makefile 55 | 56 | create_makefile 'qml/qml' 57 | -------------------------------------------------------------------------------- /ext/qml/js_array.c: -------------------------------------------------------------------------------- 1 | #include "js_array.h" 2 | #include "conversion.h" 3 | #include "engine.h" 4 | 5 | VALUE rbqml_cJSArray; 6 | 7 | /* 8 | * @return [Integer] 9 | */ 10 | static VALUE js_array_length(VALUE self) 11 | { 12 | qmlbind_value *array = rbqml_js_object_get(self); 13 | 14 | qmlbind_value *lenValue = qmlbind_value_get_property(array, "length"); 15 | int len = qmlbind_value_get_number(lenValue); 16 | qmlbind_value_release(lenValue); 17 | 18 | return INT2NUM(len); 19 | } 20 | 21 | static VALUE js_array_each(VALUE self) 22 | { 23 | RETURN_SIZED_ENUMERATOR(self, 0, 0, &js_array_length); 24 | 25 | qmlbind_value *array = rbqml_js_object_get(self); 26 | 27 | qmlbind_value *lenValue = qmlbind_value_get_property(array, "length"); 28 | int len = qmlbind_value_get_number(lenValue); 29 | qmlbind_value_release(lenValue); 30 | 31 | for (int i = 0; i < len; ++i) { 32 | qmlbind_value *elem = qmlbind_value_get_array_item(array, i); 33 | VALUE rubyElem = rb_ensure(&rbqml_to_ruby, (VALUE)elem, (VALUE (*)())&qmlbind_value_release, (VALUE)elem); 34 | rb_yield(rubyElem); 35 | } 36 | 37 | return self; 38 | } 39 | 40 | void rbqml_init_js_array(void) 41 | { 42 | VALUE mQML = rb_define_module("QML"); 43 | rbqml_cJSArray = rb_define_class_under(mQML, "JSArray", rbqml_cJSObject); 44 | 45 | rb_define_method(rbqml_cJSArray, "length", js_array_length, 0); 46 | rb_define_method(rbqml_cJSArray, "each", js_array_each, 0); 47 | } 48 | -------------------------------------------------------------------------------- /ext/qml/js_array.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "js_object.h" 4 | 5 | extern VALUE rbqml_cJSArray; 6 | 7 | void rbqml_init_js_array(void); 8 | -------------------------------------------------------------------------------- /ext/qml/js_function.c: -------------------------------------------------------------------------------- 1 | #include "js_function.h" 2 | #include "conversion.h" 3 | #include "engine.h" 4 | 5 | VALUE rbqml_cJSFunction; 6 | 7 | typedef enum { 8 | CallFunction, CallMethod, CallConstructor 9 | } CallType; 10 | 11 | typedef struct { 12 | qmlbind_value *func; 13 | qmlbind_value *instance; 14 | int argc; 15 | const qmlbind_value *const *argv; 16 | CallType type; 17 | } function_call_data; 18 | 19 | static void *function_call_impl(void *p) { 20 | function_call_data *data = p; 21 | 22 | switch (data->type) { 23 | case CallFunction: 24 | return qmlbind_value_call(data->func, data->argc, data->argv); 25 | case CallMethod: { 26 | return qmlbind_value_call_with_instance(data->func, data->instance, data->argc, data->argv); 27 | } 28 | case CallConstructor: { 29 | return qmlbind_value_call_constructor(data->func, data->argc, data->argv); 30 | } 31 | } 32 | return NULL; 33 | } 34 | 35 | static VALUE function_call(VALUE self, VALUE thisValue, VALUE args, CallType callType) { 36 | qmlbind_value *func = rbqml_js_object_get(self); 37 | 38 | int argc = RARRAY_LEN(args); 39 | const qmlbind_value **qmlArgs = malloc(argc * sizeof(qmlbind_value *)); 40 | 41 | for (int i = 0; i < argc; ++i) { 42 | qmlbind_value *value = rbqml_to_qml(RARRAY_AREF(args, i)); 43 | qmlArgs[i] = value; 44 | } 45 | 46 | function_call_data data; 47 | data.func = func; 48 | if (callType == CallMethod) { 49 | data.instance = rbqml_js_object_get(thisValue); 50 | } 51 | data.argc = argc; 52 | data.argv = qmlArgs; 53 | data.type = callType; 54 | 55 | qmlbind_value *result = rb_thread_call_without_gvl(&function_call_impl, &data, RUBY_UBF_IO, NULL); 56 | 57 | bool is_error = qmlbind_value_is_error(result); 58 | VALUE resultValue = rbqml_to_ruby(result); 59 | qmlbind_value_release(result); 60 | qmlbind_value_release(func); 61 | 62 | if (is_error) { 63 | rb_exc_raise(rb_funcall(resultValue, rb_intern("to_error"), 0)); 64 | } 65 | return resultValue; 66 | } 67 | 68 | static VALUE js_function_call(int argc, VALUE *argv, VALUE self) { 69 | VALUE args; 70 | VALUE block; 71 | rb_scan_args(argc, argv, "*&", &args, &block); 72 | if (!NIL_P(block)) { 73 | args = rb_ary_concat(args, rb_ary_new_from_args(1, block)); 74 | } 75 | 76 | return function_call(self, Qnil, args, CallFunction); 77 | } 78 | 79 | static VALUE js_function_new(int argc, VALUE *argv, VALUE self) { 80 | VALUE args; 81 | VALUE block; 82 | rb_scan_args(argc, argv, "*&", &args, &block); 83 | if (!NIL_P(block)) { 84 | args = rb_ary_concat(args, rb_ary_new_from_args(1, block)); 85 | } 86 | 87 | return function_call(self, Qnil, args, CallConstructor); 88 | } 89 | 90 | static VALUE js_function_call_with_instance(int argc, VALUE *argv, VALUE self) { 91 | VALUE thisValue; 92 | VALUE args; 93 | VALUE block; 94 | rb_scan_args(argc, argv, "1*&", &thisValue, &args, &block); 95 | if (!NIL_P(block)) { 96 | args = rb_ary_concat(args, rb_ary_new_from_args(1, block)); 97 | } 98 | 99 | return function_call(self, thisValue, args, CallMethod); 100 | } 101 | 102 | void rbqml_init_js_function(void) { 103 | VALUE mQML = rb_define_module("QML"); 104 | rbqml_cJSFunction = rb_define_class_under(mQML, "JSFunction", rbqml_cJSObject); 105 | 106 | rb_define_method(rbqml_cJSFunction, "call", js_function_call, -1); 107 | rb_define_method(rbqml_cJSFunction, "new", js_function_new, -1); 108 | rb_define_method(rbqml_cJSFunction, "call_with_instance", js_function_call_with_instance, -1); 109 | } 110 | -------------------------------------------------------------------------------- /ext/qml/js_function.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "js_object.h" 4 | 5 | extern VALUE rbqml_cJSFunction; 6 | 7 | void rbqml_init_js_function(void); 8 | -------------------------------------------------------------------------------- /ext/qml/js_object.c: -------------------------------------------------------------------------------- 1 | #include "js_object.h" 2 | #include "engine.h" 3 | #include "conversion.h" 4 | 5 | typedef struct { 6 | const qmlbind_value *obj; 7 | const char *key; 8 | } get_property_data; 9 | 10 | void *get_property_impl(void *p) { 11 | get_property_data *data = p; 12 | return qmlbind_value_get_property(data->obj, data->key); 13 | } 14 | 15 | qmlbind_value *rbqml_get_property(const qmlbind_value *obj, const char *key) { 16 | get_property_data data; 17 | data.obj = obj; 18 | data.key = key; 19 | return rb_thread_call_without_gvl(&get_property_impl, &data, RUBY_UBF_IO, NULL); 20 | } 21 | 22 | typedef struct { 23 | const qmlbind_value *obj; 24 | int index; 25 | } get_array_item_data; 26 | 27 | void *get_array_item_impl(void *p) { 28 | get_array_item_data *data = p; 29 | return qmlbind_value_get_array_item(data->obj, data->index); 30 | } 31 | 32 | qmlbind_value *rbqml_get_array_item(const qmlbind_value *obj, int index) { 33 | get_array_item_data data; 34 | data.obj = obj; 35 | data.index = index; 36 | return rb_thread_call_without_gvl(&get_array_item_impl, &data, RUBY_UBF_IO, NULL); 37 | } 38 | 39 | typedef struct { 40 | qmlbind_value *obj; 41 | const char *key; 42 | const qmlbind_value *value; 43 | } set_property_data; 44 | 45 | void *set_property_impl(void *p) { 46 | set_property_data *data = p; 47 | qmlbind_value_set_property(data->obj, data->key, data->value); 48 | return NULL; 49 | } 50 | 51 | void rbqml_set_property(qmlbind_value *obj, const char *key, const qmlbind_value *value) { 52 | set_property_data data; 53 | data.obj = obj; 54 | data.key = key; 55 | data.value = value; 56 | rb_thread_call_without_gvl(&set_property_impl, &data, RUBY_UBF_IO, NULL); 57 | } 58 | 59 | typedef struct { 60 | qmlbind_value *obj; 61 | int index; 62 | const qmlbind_value *value; 63 | } set_array_item_data; 64 | 65 | void *set_array_item_impl(void *p) { 66 | set_array_item_data *data = p; 67 | qmlbind_value_set_array_item(data->obj, data->index, data->value); 68 | return NULL; 69 | } 70 | 71 | void rbqml_set_array_item(qmlbind_value *obj, int index, const qmlbind_value *value) { 72 | set_array_item_data data; 73 | data.obj = obj; 74 | data.index = index; 75 | data.value = value; 76 | rb_thread_call_without_gvl(&set_array_item_impl, &data, RUBY_UBF_IO, NULL); 77 | } 78 | 79 | 80 | qmlbind_value *rbqml_get_iterator_value(const qmlbind_iterator *iter) { 81 | return rb_thread_call_without_gvl((void *(*)(void *))&qmlbind_iterator_get_value, (void *)iter, RUBY_UBF_IO, NULL); 82 | } 83 | 84 | typedef struct { 85 | qmlbind_value *value; 86 | } js_object_t; 87 | 88 | VALUE rbqml_cJSObject; 89 | 90 | void free_js_object(void *ptr) 91 | { 92 | js_object_t *obj = ptr; 93 | qmlbind_value_release(obj->value); 94 | xfree(obj); 95 | } 96 | 97 | static const rb_data_type_t data_type = { 98 | "QML::JSObject", 99 | { NULL, &free_js_object } 100 | }; 101 | 102 | static VALUE js_object_alloc(VALUE klass) 103 | { 104 | return rbqml_js_object_new(klass, qmlbind_value_new_null()); 105 | } 106 | 107 | VALUE rbqml_js_object_new(VALUE klass, qmlbind_value *value) 108 | { 109 | js_object_t *obj = ALLOC(js_object_t); 110 | obj->value = qmlbind_value_clone(value); 111 | return TypedData_Wrap_Struct(klass, &data_type, obj); 112 | } 113 | 114 | bool rbqml_js_object_p(VALUE value) 115 | { 116 | return rb_type(value) == T_DATA && RTYPEDDATA_P(value) && RTYPEDDATA_TYPE(value) == &data_type; 117 | } 118 | 119 | qmlbind_value *rbqml_js_object_get(VALUE jsobject) 120 | { 121 | js_object_t *obj; 122 | TypedData_Get_Struct(jsobject, js_object_t, &data_type, obj); 123 | return qmlbind_value_clone(obj->value); 124 | } 125 | 126 | static void get_property_key(VALUE key, int *index, const char **keyStr) 127 | { 128 | switch (rb_type(key)) { 129 | case T_FIXNUM: 130 | *index = NUM2INT(key); 131 | break; 132 | case T_STRING: 133 | *keyStr = rb_string_value_cstr(&key); 134 | break; 135 | case T_SYMBOL: 136 | *keyStr = rb_id2name(SYM2ID(key)); 137 | break; 138 | default: 139 | rb_raise(rb_eTypeError, "expected Fixnum, String or Symbol for index, got %s", rb_class2name(rb_obj_class(key))); 140 | break; 141 | } 142 | } 143 | 144 | static VALUE js_object_aref(VALUE self, VALUE key) 145 | { 146 | qmlbind_value *obj = rbqml_js_object_get(self); 147 | 148 | int index = -1; 149 | const char *keyStr; 150 | get_property_key(key, &index, &keyStr); 151 | 152 | qmlbind_value *value; 153 | 154 | if (index >= 0) { 155 | value = rbqml_get_array_item(obj, index); 156 | } else { 157 | value = rbqml_get_property(obj, keyStr); 158 | } 159 | 160 | qmlbind_value_release(obj); 161 | 162 | return rb_ensure(&rbqml_to_ruby, (VALUE)value, (VALUE (*)())&qmlbind_value_release, (VALUE)value); 163 | } 164 | 165 | static VALUE js_object_aset(VALUE self, VALUE key, VALUE value) 166 | { 167 | qmlbind_value *obj = rbqml_js_object_get(self); 168 | 169 | qmlbind_value *qmlValue = rbqml_to_qml(value); 170 | 171 | int index = -1; 172 | const char *keyStr; 173 | 174 | get_property_key(key, &index, &keyStr); 175 | 176 | if (index >= 0) { 177 | rbqml_set_array_item(obj, index, qmlValue); 178 | } else { 179 | rbqml_set_property(obj, keyStr, qmlValue); 180 | } 181 | 182 | qmlbind_value_release(obj); 183 | 184 | return value; 185 | } 186 | 187 | static VALUE js_object_each_iterator(VALUE data) 188 | { 189 | qmlbind_iterator *it = (qmlbind_iterator *)data; 190 | while (qmlbind_iterator_has_next(it)) { 191 | qmlbind_iterator_next(it); 192 | 193 | qmlbind_value *value = rbqml_get_iterator_value(it); 194 | VALUE rubyValue = rb_ensure(&rbqml_to_ruby, (VALUE)value, (VALUE (*)())&qmlbind_value_release, (VALUE)value); 195 | 196 | qmlbind_string *str = qmlbind_iterator_get_key(it); 197 | VALUE rubyKey = rb_enc_str_new(qmlbind_string_get_chars(str), qmlbind_string_get_length(str), rb_utf8_encoding()); 198 | qmlbind_string_release(str); 199 | 200 | VALUE pair[] = { rubyKey, rubyValue }; 201 | 202 | rb_yield(rb_ary_new_from_values(2, pair)); 203 | } 204 | return Qnil; 205 | } 206 | 207 | static VALUE js_object_each_pair(VALUE self) 208 | { 209 | RETURN_ENUMERATOR(self, 0, 0); 210 | 211 | qmlbind_value *obj = rbqml_js_object_get(self); 212 | 213 | qmlbind_iterator *it = qmlbind_iterator_new(obj); 214 | rb_ensure(&js_object_each_iterator, (VALUE)it, (VALUE (*)())&qmlbind_iterator_release, (VALUE)it); 215 | 216 | qmlbind_value_release(obj); 217 | 218 | return self; 219 | } 220 | 221 | static VALUE js_object_has_key_p(VALUE self, VALUE key) 222 | { 223 | qmlbind_value *obj = rbqml_js_object_get(self); 224 | 225 | int index = -1; 226 | const char *keyStr; 227 | 228 | get_property_key(key, &index, &keyStr); 229 | 230 | int ret; 231 | 232 | if (index >= 0) { 233 | ret = qmlbind_value_has_index(obj, index); 234 | } else { 235 | ret = qmlbind_value_has_property(obj, keyStr); 236 | } 237 | 238 | qmlbind_value_release(obj); 239 | 240 | if (ret) { 241 | return Qtrue; 242 | } else { 243 | return Qfalse; 244 | } 245 | } 246 | 247 | static VALUE js_object_error_p(VALUE self) 248 | { 249 | qmlbind_value *obj = rbqml_js_object_get(self); 250 | if (qmlbind_value_is_error(obj)) { 251 | return Qtrue; 252 | } else { 253 | return Qfalse; 254 | } 255 | } 256 | 257 | /* 258 | * Compares two JS objects by JavaScript identity (===). 259 | * @return [Boolean] 260 | */ 261 | static VALUE js_object_equal_p(VALUE self, VALUE other) 262 | { 263 | qmlbind_value *obj = rbqml_js_object_get(self); 264 | qmlbind_value *otherObj = rbqml_js_object_get(other); 265 | 266 | if (qmlbind_value_is_identical(obj, otherObj)) { 267 | return Qtrue; 268 | } else { 269 | return Qfalse; 270 | } 271 | } 272 | 273 | void rbqml_init_js_object(void) 274 | { 275 | VALUE mQML = rb_define_module("QML"); 276 | rbqml_cJSObject = rb_define_class_under(mQML, "JSObject", rb_cObject); 277 | 278 | rb_define_method(rbqml_cJSObject, "[]", js_object_aref, 1); 279 | rb_define_method(rbqml_cJSObject, "[]=", js_object_aset, 2); 280 | rb_define_method(rbqml_cJSObject, "each_pair", js_object_each_pair, 0); 281 | rb_define_method(rbqml_cJSObject, "has_key?", js_object_has_key_p, 1); 282 | rb_define_method(rbqml_cJSObject, "error?", js_object_error_p, 0); 283 | rb_define_method(rbqml_cJSObject, "==", js_object_equal_p, 1); 284 | } 285 | -------------------------------------------------------------------------------- /ext/qml/js_object.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "qml.h" 4 | #include 5 | 6 | qmlbind_value *rbqml_get_property(const qmlbind_value *obj, const char *key); 7 | qmlbind_value *rbqml_get_array_item(const qmlbind_value *obj, int index); 8 | void rbqml_set_property(qmlbind_value *obj, const char *key, const qmlbind_value *value); 9 | void rbqml_set_array_item(qmlbind_value *obj, int index, const qmlbind_value *value); 10 | qmlbind_value *rbqml_get_iterator_value(const qmlbind_iterator *iter); 11 | 12 | VALUE rbqml_js_object_new(VALUE klass, qmlbind_value *value); 13 | 14 | bool rbqml_js_object_p(VALUE value); 15 | qmlbind_value *rbqml_js_object_get(VALUE jsobject); 16 | 17 | extern VALUE rbqml_cJSObject; 18 | 19 | void rbqml_init_js_object(void); 20 | -------------------------------------------------------------------------------- /ext/qml/js_wrapper.c: -------------------------------------------------------------------------------- 1 | #include "js_wrapper.h" 2 | 3 | VALUE rbqml_cJSWrapper; 4 | 5 | /* 6 | * @return [QML::Access] 7 | */ 8 | static VALUE js_wrapper_unwrap(VALUE self) { 9 | qmlbind_value *wrapper = rbqml_js_object_get(self); 10 | VALUE unwrapped = (VALUE)qmlbind_value_unwrap(wrapper); 11 | qmlbind_value_release(wrapper); 12 | return unwrapped; 13 | } 14 | 15 | void rbqml_init_js_wrapper(void) { 16 | VALUE mQML = rb_define_module("QML"); 17 | rbqml_cJSWrapper = rb_define_class_under(mQML, "JSWrapper", rbqml_cJSObject); 18 | rb_define_method(rbqml_cJSWrapper, "unwrap", js_wrapper_unwrap, 0); 19 | } 20 | -------------------------------------------------------------------------------- /ext/qml/js_wrapper.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "js_object.h" 4 | 5 | extern VALUE rbqml_cJSWrapper; 6 | 7 | void rbqml_init_js_wrapper(void); 8 | -------------------------------------------------------------------------------- /ext/qml/meta_class.c: -------------------------------------------------------------------------------- 1 | #include "meta_class.h" 2 | #include "conversion.h" 3 | #include "application.h" 4 | #include "engine.h" 5 | #include "refcounter.h" 6 | #include "signal_emitter.h" 7 | 8 | VALUE rbqml_cMetaClass; 9 | VALUE rbqml_mInterface; 10 | 11 | typedef struct { 12 | qmlbind_client_class *class_handle; 13 | qmlbind_signal_emitter *emitter; 14 | } new_object_data; 15 | 16 | static void *new_object_impl(void *p) { 17 | new_object_data *data = p; 18 | 19 | VALUE klass = (VALUE)data->class_handle; 20 | VALUE obj = rb_funcall(klass, rb_intern("new"), 0); 21 | VALUE emitterValue = rbqml_signal_emitter_new(data->emitter); 22 | rb_funcall(obj, rb_intern("set_signal_emitter"), 1, emitterValue); 23 | 24 | rbqml_retain_value(obj); 25 | return (void *)obj; 26 | } 27 | 28 | static qmlbind_client_object *new_object(qmlbind_client_class *class_handle, qmlbind_signal_emitter *emitter) { 29 | new_object_data data; 30 | data.class_handle = class_handle; 31 | data.emitter = emitter; 32 | 33 | return rb_thread_call_with_gvl(&new_object_impl, &data); 34 | } 35 | 36 | static void delete_object(qmlbind_client_object *handle) { 37 | rb_thread_call_with_gvl((void *(*)(void *))rbqml_release_value, handle); 38 | } 39 | 40 | typedef struct { 41 | qmlbind_client_object *backref; 42 | const char *name; 43 | int argc; 44 | const qmlbind_value *const *argv; 45 | } call_method_data; 46 | 47 | static void *call_method_impl(void *p) { 48 | call_method_data *data = p; 49 | 50 | VALUE obj = (VALUE)data->backref; 51 | VALUE method = ID2SYM(rb_intern(data->name)); 52 | 53 | VALUE *args = alloca(data->argc * sizeof(VALUE)); 54 | for (int i = 0; i < data->argc; ++i) { 55 | args[i] = rbqml_to_ruby(data->argv[i]); 56 | } 57 | 58 | VALUE result = rb_funcall(rbqml_mInterface, rb_intern("call_method"), 3, 59 | obj, method, rb_ary_new_from_values(data->argc, args)); 60 | 61 | return rbqml_to_qml(result); 62 | } 63 | 64 | static qmlbind_value *call_method( 65 | qmlbind_engine *engine, 66 | qmlbind_client_object *handle, const char *name, 67 | int argc, const qmlbind_value *const *argv) { 68 | 69 | call_method_data data; 70 | data.backref = handle; 71 | data.name = name; 72 | data.argc = argc; 73 | data.argv = argv; 74 | 75 | return rb_thread_call_with_gvl(&call_method_impl, &data); 76 | } 77 | 78 | typedef struct { 79 | qmlbind_client_object *handle; 80 | const char *name; 81 | } get_property_data; 82 | 83 | static void *get_property_impl(void *p) { 84 | get_property_data *data = p; 85 | 86 | VALUE obj = (VALUE)data->handle; 87 | VALUE method = ID2SYM(rb_intern(data->name)); 88 | 89 | VALUE result = rb_funcall(rbqml_mInterface, rb_intern("call_method"), 3, 90 | obj, method, rb_ary_new()); 91 | 92 | return rbqml_to_qml(result); 93 | } 94 | 95 | static qmlbind_value *get_property( 96 | qmlbind_engine *engine, 97 | qmlbind_client_object *handle, const char *name) { 98 | 99 | get_property_data data; 100 | data.handle = handle; 101 | data.name = name; 102 | 103 | return rb_thread_call_with_gvl(&get_property_impl, &data); 104 | } 105 | 106 | typedef struct { 107 | qmlbind_client_object *handle; 108 | const char *name; 109 | const qmlbind_value *value; 110 | } set_property_data; 111 | 112 | static void *set_property_impl(void *p) { 113 | set_property_data* data = p; 114 | 115 | VALUE obj = (VALUE)data->handle; 116 | VALUE method = rb_str_intern(rb_sprintf("%s=", data->name)); 117 | 118 | VALUE ruby_value = rbqml_to_ruby(data->value); 119 | 120 | rb_funcall(rbqml_mInterface, rb_intern("call_method"), 3, 121 | obj, method, rb_ary_new_from_args(1, ruby_value)); 122 | 123 | return NULL; 124 | } 125 | 126 | static void set_property( 127 | qmlbind_engine *engine, 128 | qmlbind_client_object *handle, const char *name, const qmlbind_value *value) { 129 | 130 | set_property_data data; 131 | data.handle = handle; 132 | data.name = name; 133 | data.value = value; 134 | 135 | rb_thread_call_with_gvl(&set_property_impl, &data); 136 | } 137 | 138 | qmlbind_client_callbacks handlers = { 139 | &new_object, &delete_object, &call_method, &get_property, &set_property 140 | }; 141 | 142 | typedef struct { 143 | qmlbind_metaclass *metaclass; 144 | } meta_class; 145 | 146 | static void meta_class_free(void *p) { 147 | meta_class *data = (meta_class *)p; 148 | 149 | rb_thread_call_without_gvl((void *(*)(void *))&qmlbind_metaclass_release, data->metaclass, RUBY_UBF_IO, NULL); 150 | xfree(data); 151 | } 152 | 153 | static const rb_data_type_t data_type = { 154 | "QML::MetaClass", 155 | { NULL, &meta_class_free } 156 | }; 157 | 158 | qmlbind_metaclass *rbqml_get_metaclass(VALUE self) { 159 | meta_class *data; 160 | TypedData_Get_Struct(self, meta_class, &data_type, data); 161 | return data->metaclass; 162 | } 163 | 164 | static VALUE meta_class_alloc(VALUE klass) { 165 | meta_class *data = ALLOC(meta_class); 166 | data->metaclass = NULL; 167 | return TypedData_Wrap_Struct(klass, &data_type, data); 168 | } 169 | 170 | static VALUE meta_class_initialize(VALUE self, VALUE klass, VALUE name) { 171 | meta_class *data; 172 | TypedData_Get_Struct(self, meta_class, &data_type, data); 173 | data->metaclass = qmlbind_metaclass_new((qmlbind_client_class *)klass, rb_string_value_cstr(&name), handlers); 174 | rb_gc_register_mark_object(klass); 175 | 176 | return self; 177 | } 178 | 179 | static VALUE meta_class_add_method(VALUE self, VALUE name, VALUE arity) { 180 | qmlbind_metaclass *metaclass = rbqml_get_metaclass(self); 181 | qmlbind_metaclass_add_method(metaclass, rb_id2name(SYM2ID(name)), NUM2INT(arity)); 182 | return Qnil; 183 | } 184 | 185 | static VALUE meta_class_add_signal(VALUE self, VALUE name, VALUE params) { 186 | qmlbind_metaclass *metaclass = rbqml_get_metaclass(self); 187 | 188 | int arity = RARRAY_LEN(params); 189 | 190 | const char **paramStrs = alloca(arity * sizeof(char *)); 191 | for (int i = 0; i < arity; ++i) { 192 | paramStrs[i] = rb_id2name(SYM2ID(RARRAY_AREF(params, i))); 193 | } 194 | 195 | qmlbind_metaclass_add_signal(metaclass, rb_id2name(SYM2ID(name)), arity, paramStrs); 196 | return Qnil; 197 | } 198 | 199 | static VALUE meta_class_add_property(VALUE self, VALUE name, VALUE notifier) { 200 | qmlbind_metaclass *metaclass = rbqml_get_metaclass(self); 201 | qmlbind_metaclass_add_property(metaclass, rb_id2name(SYM2ID(name)), rb_id2name(SYM2ID(notifier))); 202 | return Qnil; 203 | } 204 | 205 | typedef struct { 206 | qmlbind_engine *engine; 207 | qmlbind_metaclass *metaclass; 208 | qmlbind_client_object *handle; 209 | } wrap_data; 210 | 211 | void *wrap_impl(void *p) { 212 | wrap_data *data = p; 213 | return qmlbind_engine_new_wrapper(data->engine, data->metaclass, data->handle); 214 | } 215 | 216 | VALUE meta_class_wrap(VALUE self, VALUE access) { 217 | wrap_data data; 218 | data.engine = rbqml_get_engine(rbqml_engine); 219 | data.metaclass = rbqml_get_metaclass(self); 220 | data.handle = (qmlbind_client_object *)access; 221 | 222 | qmlbind_value *wrapped = rb_thread_call_without_gvl(wrap_impl, &data, RUBY_UBF_IO, NULL); 223 | VALUE ret = rbqml_to_ruby(wrapped); 224 | qmlbind_value_release(wrapped); 225 | return ret; 226 | } 227 | 228 | static VALUE meta_class_register(VALUE self, VALUE uri, VALUE versionMajor, VALUE versionMinor, VALUE qmlName) { 229 | qmlbind_metaclass *metaclass = rbqml_get_metaclass(self); 230 | qmlbind_metaclass_register( 231 | metaclass, 232 | rb_string_value_cstr(&uri), 233 | NUM2INT(versionMajor), NUM2INT(versionMinor), 234 | rb_string_value_cstr(&qmlName)); 235 | return self; 236 | } 237 | 238 | void rbqml_init_meta_class() { 239 | VALUE mQML = rb_define_module("QML"); 240 | rbqml_cMetaClass = rb_define_class_under(mQML, "MetaClass", rb_cObject); 241 | rb_require("qml/interface"); 242 | rbqml_mInterface = rb_path2class("QML::Interface"); 243 | 244 | rb_define_alloc_func(rbqml_cMetaClass, &meta_class_alloc); 245 | 246 | rb_define_private_method(rbqml_cMetaClass, "initialize", meta_class_initialize, 2); 247 | rb_define_method(rbqml_cMetaClass, "add_method", meta_class_add_method, 2); 248 | rb_define_method(rbqml_cMetaClass, "add_signal", meta_class_add_signal, 2); 249 | rb_define_method(rbqml_cMetaClass, "add_property", meta_class_add_property, 2); 250 | rb_define_method(rbqml_cMetaClass, "wrap", meta_class_wrap, 1); 251 | rb_define_method(rbqml_cMetaClass, "register", meta_class_register, 4); 252 | } 253 | -------------------------------------------------------------------------------- /ext/qml/meta_class.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | void rbqml_init_meta_class(); 8 | -------------------------------------------------------------------------------- /ext/qml/plugin_loader.c: -------------------------------------------------------------------------------- 1 | #include "plugin_loader.h" 2 | #include "engine.h" 3 | #include "conversion.h" 4 | 5 | static VALUE cPluginError; 6 | VALUE rbqml_cPluginLoader; 7 | 8 | typedef struct { 9 | qmlbind_plugin *plugin; 10 | } plugin_loader_t; 11 | 12 | static void plugin_loader_free(void *p) { 13 | plugin_loader_t *data = (plugin_loader_t*)p; 14 | qmlbind_plugin_release(data->plugin); 15 | xfree(data); 16 | } 17 | 18 | static const rb_data_type_t data_type = { 19 | "QML::PluginLoader", 20 | { NULL, &plugin_loader_free } 21 | }; 22 | 23 | qmlbind_plugin *rbqml_get_plugin(VALUE self) { 24 | plugin_loader_t *data; 25 | TypedData_Get_Struct(self, plugin_loader_t, &data_type, data); 26 | return data->plugin; 27 | } 28 | 29 | static VALUE plugin_loader_alloc(VALUE klass) { 30 | plugin_loader_t *data = ALLOC(plugin_loader_t); 31 | data->plugin = NULL; 32 | return TypedData_Wrap_Struct(klass, &data_type, data); 33 | } 34 | 35 | static VALUE plugin_loader_init(VALUE self, VALUE path) { 36 | plugin_loader_t *data; 37 | TypedData_Get_Struct(self, plugin_loader_t, &data_type, data); 38 | data->plugin = qmlbind_plugin_new(rb_string_value_cstr(&path)); 39 | return self; 40 | } 41 | 42 | /* 43 | * Loads the plugin and returns the instance of the plugin. 44 | * @return [QML::JSObject] 45 | */ 46 | static VALUE plugin_loader_instance(VALUE self) { 47 | qmlbind_plugin *plugin = rbqml_get_plugin(self); 48 | 49 | qmlbind_string *qmlerror = qmlbind_plugin_get_error_string(plugin); 50 | if (qmlerror) { 51 | VALUE errorStr = rb_enc_str_new(qmlbind_string_get_chars(qmlerror), qmlbind_string_get_length(qmlerror), rb_utf8_encoding()); 52 | qmlbind_string_release(qmlerror); 53 | 54 | VALUE error = rb_funcall(cPluginError, rb_intern("new"), 1, errorStr); 55 | rb_exc_raise(error); 56 | } 57 | 58 | qmlbind_value *loaded = qmlbind_plugin_get_instance(plugin, rbqml_get_engine(rbqml_engine)); 59 | VALUE ret = rbqml_to_ruby(loaded); 60 | qmlbind_value_release(loaded); 61 | return ret; 62 | } 63 | 64 | void rbqml_init_plugin_loader(void) { 65 | cPluginError = rb_path2class("QML::PluginError"); 66 | 67 | VALUE mQML = rb_define_module("QML"); 68 | rbqml_cPluginLoader = rb_define_class_under(mQML, "PluginLoader", rb_cObject); 69 | rb_define_alloc_func(rbqml_cPluginLoader, &plugin_loader_alloc); 70 | 71 | rb_define_private_method(rbqml_cPluginLoader, "initialize_impl", plugin_loader_init, 1); 72 | rb_define_method(rbqml_cPluginLoader, "instance", plugin_loader_instance, 0); 73 | } 74 | -------------------------------------------------------------------------------- /ext/qml/plugin_loader.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "qml.h" 4 | 5 | extern VALUE rbqml_cPluginLoader; 6 | 7 | void rbqml_init_plugin_loader(); 8 | -------------------------------------------------------------------------------- /ext/qml/qml.c: -------------------------------------------------------------------------------- 1 | #include "qml.h" 2 | #include "application.h" 3 | #include "component.h" 4 | #include "engine.h" 5 | #include "meta_class.h" 6 | #include "js_object.h" 7 | #include "js_array.h" 8 | #include "js_function.h" 9 | #include "js_wrapper.h" 10 | #include "signal_emitter.h" 11 | #include "plugin_loader.h" 12 | #include "refcounter.h" 13 | 14 | VALUE rbqml_mQML; 15 | VALUE rbqml_application = Qnil; 16 | VALUE rbqml_engine = Qnil; 17 | 18 | /* 19 | * @api private 20 | */ 21 | static VALUE qml_init(VALUE module, VALUE args) { 22 | if (!NIL_P(rbqml_application)) { 23 | rb_raise(rb_eRuntimeError, "QML already initialized"); 24 | } 25 | 26 | rbqml_application = rb_funcall(rbqml_cApplication, rb_intern("new"), 1, args); 27 | rbqml_engine = rb_funcall(rbqml_cEngine, rb_intern("new"), 0); 28 | 29 | rb_gc_register_address(&rbqml_application); 30 | rb_gc_register_address(&rbqml_engine); 31 | 32 | VALUE blocks = rb_const_get(module, rb_intern("INIT_BLOCKS")); 33 | for (int i = 0; i < RARRAY_LEN(blocks); ++i) { 34 | rb_proc_call(RARRAY_AREF(blocks, i), rb_ary_new()); 35 | } 36 | 37 | return module; 38 | } 39 | 40 | static VALUE qml_initialized_p(VALUE module) { 41 | if (NIL_P(rbqml_application)) { 42 | return Qfalse; 43 | } else { 44 | return Qtrue; 45 | } 46 | } 47 | 48 | /* 49 | * Returns the instance of {Application}. 50 | * @return [Application] 51 | */ 52 | static VALUE qml_application(VALUE module) { 53 | if (NIL_P(rbqml_application)) { 54 | rb_raise(rb_eRuntimeError, "QML not yet initialized"); 55 | } 56 | return rbqml_application; 57 | } 58 | 59 | /* 60 | * Returns the instance of {Engine}. 61 | * @return [Engine] 62 | */ 63 | static VALUE qml_engine(VALUE module) { 64 | if (NIL_P(rbqml_engine)) { 65 | rb_raise(rb_eRuntimeError, "QML not yet initialized"); 66 | } 67 | return rbqml_engine; 68 | } 69 | 70 | static void *nextTickCallbackImpl(void *data) 71 | { 72 | VALUE block = (VALUE)data; 73 | rb_proc_call(block, rb_ary_new()); 74 | rbqml_release_value(block); 75 | return NULL; 76 | } 77 | 78 | static void nextTickCallback(void *data) 79 | { 80 | rb_thread_call_with_gvl(nextTickCallbackImpl, data); 81 | } 82 | 83 | static VALUE qml_next_tick(int argc, VALUE *argv, VALUE module) { 84 | VALUE block; 85 | rb_scan_args(argc, argv, "&", &block); 86 | rbqml_retain_value(block); 87 | 88 | qmlbind_next_tick(nextTickCallback, (void *)block); 89 | return block; 90 | } 91 | 92 | void Init_qml(void) 93 | { 94 | rbqml_mQML = rb_define_module("QML"); 95 | 96 | rbqml_init_refcounter(); 97 | rbqml_init_application(); 98 | rbqml_init_engine(); 99 | rbqml_init_component(); 100 | rbqml_init_meta_class(); 101 | rbqml_init_js_object(); 102 | rbqml_init_js_array(); 103 | rbqml_init_js_function(); 104 | rbqml_init_js_wrapper(); 105 | rbqml_init_signal_emitter(); 106 | rbqml_init_plugin_loader(); 107 | 108 | rb_define_module_function(rbqml_mQML, "initialized?", qml_initialized_p, 0); 109 | rb_define_module_function(rbqml_mQML, "init_impl", qml_init, 1); 110 | rb_define_module_function(rbqml_mQML, "application", qml_application, 0); 111 | rb_define_module_function(rbqml_mQML, "engine", qml_engine, 0); 112 | rb_define_module_function(rbqml_mQML, "next_tick", qml_next_tick, -1); 113 | } 114 | -------------------------------------------------------------------------------- /ext/qml/qml.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | extern VALUE rbqml_mQML; 9 | extern VALUE rbqml_application; 10 | extern VALUE rbqml_engine; 11 | -------------------------------------------------------------------------------- /ext/qml/refcounter.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "refcounter.h" 4 | 5 | static std::unordered_map ref_counts; 6 | 7 | static void mark_values(void *) 8 | { 9 | for (auto &&kv : ref_counts) { 10 | rb_gc_mark(kv.first); 11 | } 12 | } 13 | 14 | static const rb_data_type_t marker_type = { 15 | "QML::RefCounterMarker", 16 | { &mark_values } 17 | }; 18 | static VALUE marker_class; 19 | static VALUE marker; 20 | 21 | extern "C" { 22 | 23 | void rbqml_init_refcounter() 24 | { 25 | marker_class = rb_define_class_under(rb_path2class("QML"), "RefCounterMarker", rb_cObject); 26 | marker = TypedData_Wrap_Struct(marker_class, &marker_type, nullptr); 27 | rb_gc_register_address(&marker); 28 | } 29 | 30 | void rbqml_retain_value(VALUE value) 31 | { 32 | if (ref_counts.find(value) == ref_counts.end()) { 33 | ref_counts[value] = 1; 34 | } else { 35 | ++ref_counts[value]; 36 | } 37 | } 38 | 39 | void rbqml_release_value(VALUE handle) 40 | { 41 | if (ref_counts[handle] <= 1) { 42 | ref_counts.erase(handle); 43 | } 44 | --ref_counts[handle]; 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /ext/qml/refcounter.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #ifdef __cplusplus 7 | extern "C" { 8 | #endif 9 | 10 | void rbqml_init_refcounter(); 11 | 12 | void rbqml_retain_value(VALUE value); 13 | void rbqml_release_value(VALUE handle); 14 | 15 | #ifdef __cplusplus 16 | } 17 | #endif 18 | -------------------------------------------------------------------------------- /ext/qml/rubyqml-plugin/listmodel.cpp: -------------------------------------------------------------------------------- 1 | #include "listmodel.h" 2 | #include 3 | #include 4 | 5 | namespace RubyQml { 6 | 7 | static QJSValue callMethod(QJSValue self, const QString &name, const QJSValueList &args) 8 | { 9 | QJSValue func = self.property(name); 10 | return func.callWithInstance(self, args); 11 | } 12 | 13 | ListModel::ListModel(const QJSValue &rubyModelAccess, QObject *parent) : QAbstractListModel(parent), 14 | mRubyModelAccess(rubyModelAccess) 15 | { 16 | QObject *access = rubyModelAccess.toQObject(); 17 | Q_ASSERT(access); 18 | QQmlEngine::setObjectOwnership(access, QQmlEngine::CppOwnership); 19 | access->setParent(this); 20 | 21 | QVariantList columns = callMethod(rubyModelAccess, "columns", QJSValueList()).toVariant().toList(); 22 | for (int i = 0; i < columns.size(); ++i) { 23 | mRoleNames[Qt::UserRole + i] = columns[i].toString().toUtf8(); 24 | } 25 | 26 | connect(access, SIGNAL(begin_insert(QJSValue,QJSValue)), this, SLOT(beginInsert(QJSValue,QJSValue))); 27 | connect(access, SIGNAL(end_insert()), this, SLOT(endInsert())); 28 | connect(access, SIGNAL(begin_remove(QJSValue,QJSValue)), this, SLOT(beginRemove(QJSValue,QJSValue))); 29 | connect(access, SIGNAL(end_remove()), this, SLOT(endRemove())); 30 | connect(access, SIGNAL(begin_move(QJSValue,QJSValue,QJSValue)), this, SLOT(beginMove(QJSValue,QJSValue,QJSValue))); 31 | connect(access, SIGNAL(end_move()), this, SLOT(endMove())); 32 | connect(access, SIGNAL(update(QJSValue,QJSValue)), this, SLOT(update(QJSValue,QJSValue))); 33 | connect(access, SIGNAL(begin_reset()), this, SLOT(beginReset())); 34 | connect(access, SIGNAL(end_reset()), this, SLOT(endReset())); 35 | } 36 | 37 | QHash ListModel::roleNames() const 38 | { 39 | return mRoleNames; 40 | } 41 | 42 | QVariant ListModel::data(const QModelIndex &index, int role) const 43 | { 44 | QJSValueList args; 45 | args << index.row(); 46 | args << QString::fromUtf8(mRoleNames[role]); 47 | return QVariant::fromValue(callMethod(mRubyModelAccess, "data", args)); 48 | } 49 | 50 | int ListModel::rowCount(const QModelIndex &parent) const 51 | { 52 | Q_UNUSED(parent); 53 | return callMethod(mRubyModelAccess, "count", QJSValueList()).toInt(); 54 | } 55 | 56 | void ListModel::beginMove(const QJSValue &first, const QJSValue &last, const QJSValue &destination) 57 | { 58 | beginMoveRows(QModelIndex(), first.toInt(), last.toInt(), QModelIndex(), destination.toInt()); 59 | } 60 | 61 | void ListModel::endMove() 62 | { 63 | endMoveRows(); 64 | } 65 | 66 | void ListModel::beginInsert(const QJSValue &first, const QJSValue &last) 67 | { 68 | beginInsertRows(QModelIndex(), first.toInt(), last.toInt()); 69 | } 70 | 71 | void ListModel::endInsert() 72 | { 73 | endInsertRows(); 74 | } 75 | 76 | void ListModel::beginRemove(const QJSValue &first, const QJSValue &last) 77 | { 78 | beginRemoveRows(QModelIndex(), first.toInt(), last.toInt()); 79 | } 80 | 81 | void ListModel::endRemove() 82 | { 83 | endRemoveRows(); 84 | } 85 | 86 | void ListModel::beginReset() 87 | { 88 | beginResetModel(); 89 | } 90 | 91 | void ListModel::endReset() 92 | { 93 | endResetModel(); 94 | } 95 | 96 | void ListModel::update(const QJSValue &first, const QJSValue &last) 97 | { 98 | emit dataChanged(index(first.toInt()), index(last.toInt())); 99 | } 100 | 101 | } // namespace RubyQml 102 | 103 | -------------------------------------------------------------------------------- /ext/qml/rubyqml-plugin/listmodel.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | namespace RubyQml { 7 | 8 | class ListModel : public QAbstractListModel 9 | { 10 | Q_OBJECT 11 | public: 12 | explicit ListModel(const QJSValue &rubyModelAccess, QObject *parent = 0); 13 | 14 | QHash roleNames() const; 15 | QVariant data(const QModelIndex &index, int role) const; 16 | int rowCount(const QModelIndex &parent) const; 17 | 18 | signals: 19 | 20 | public slots: 21 | void beginMove(const QJSValue &first, const QJSValue &last, const QJSValue &destination); 22 | void endMove(); 23 | void beginInsert(const QJSValue &first, const QJSValue &last); 24 | void endInsert(); 25 | void beginRemove(const QJSValue &first, const QJSValue &last); 26 | void endRemove(); 27 | void beginReset(); 28 | void endReset(); 29 | void update(const QJSValue &first, const QJSValue &last); 30 | 31 | private: 32 | 33 | QHash mRoleNames; 34 | QJSValue mRubyModelAccess; 35 | }; 36 | 37 | } // namespace RubyQml 38 | -------------------------------------------------------------------------------- /ext/qml/rubyqml-plugin/rubyqml-plugin.pro: -------------------------------------------------------------------------------- 1 | #------------------------------------------------- 2 | # 3 | # Project created by QtCreator 2015-05-16T11:55:09 4 | # 5 | #------------------------------------------------- 6 | 7 | QT += core gui qml 8 | 9 | TARGET = rubyqml-plugin 10 | TEMPLATE = lib 11 | CONFIG += plugin 12 | 13 | SOURCES += \ 14 | rubyqmlplugin.cpp \ 15 | listmodel.cpp 16 | 17 | HEADERS += \ 18 | rubyqmlplugin.h \ 19 | listmodel.h 20 | -------------------------------------------------------------------------------- /ext/qml/rubyqml-plugin/rubyqmlplugin.cpp: -------------------------------------------------------------------------------- 1 | #include "rubyqmlplugin.h" 2 | #include "listmodel.h" 3 | #include 4 | #include 5 | 6 | namespace RubyQml { 7 | 8 | RubyQmlPlugin::RubyQmlPlugin(QObject *parent) : 9 | QObject(parent) 10 | { 11 | } 12 | 13 | QJSValue RubyQmlPlugin::createListModel(const QJSValue &rubyModelAccess) 14 | { 15 | QObject *access = rubyModelAccess.toQObject(); 16 | Q_ASSERT(access); 17 | QQmlEngine *engine = QQmlEngine::contextForObject(access)->engine(); 18 | 19 | ListModel *listModel = new ListModel(rubyModelAccess); 20 | QQmlEngine::setObjectOwnership(listModel, QQmlEngine::JavaScriptOwnership); 21 | 22 | return engine->newQObject(listModel); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /ext/qml/rubyqml-plugin/rubyqmlplugin.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | namespace RubyQml { 7 | 8 | class RubyQmlPlugin : public QObject 9 | { 10 | Q_OBJECT 11 | Q_PLUGIN_METADATA(IID "org.ruby-qml.RubyQml") 12 | 13 | public: 14 | RubyQmlPlugin(QObject *parent = 0); 15 | 16 | Q_INVOKABLE QJSValue createListModel(const QJSValue &rubyModelAccess); 17 | }; 18 | 19 | } 20 | -------------------------------------------------------------------------------- /ext/qml/signal_emitter.c: -------------------------------------------------------------------------------- 1 | #include "signal_emitter.h" 2 | #include "conversion.h" 3 | #include "engine.h" 4 | 5 | VALUE rbqml_cSignalEmitter; 6 | 7 | typedef struct { 8 | qmlbind_signal_emitter *emitter; 9 | } emitter_t; 10 | 11 | static void emitter_free(void *p) { 12 | emitter_t *data = (emitter_t *)p; 13 | qmlbind_signal_emitter_release(data->emitter); 14 | xfree(data); 15 | } 16 | 17 | static const rb_data_type_t data_type = { 18 | "QML::SignalEmitter", 19 | { NULL, &emitter_free } 20 | }; 21 | 22 | VALUE rbqml_signal_emitter_new(qmlbind_signal_emitter *emitter) { 23 | emitter_t *data = ALLOC(emitter_t); 24 | data->emitter = emitter; 25 | return TypedData_Wrap_Struct(rbqml_cSignalEmitter, &data_type, data); 26 | } 27 | 28 | static VALUE emitter_alloc(VALUE klass) { 29 | rb_raise(rb_eTypeError, "QML::SignalEmitter cannot be created from Ruby"); 30 | } 31 | 32 | typedef struct { 33 | qmlbind_signal_emitter *emitter; 34 | const char *name; 35 | int argc; 36 | const qmlbind_value *const *argv; 37 | } emit_data; 38 | 39 | static void *emit_impl(void *p) { 40 | emit_data *data = p; 41 | qmlbind_signal_emitter_emit(data->emitter, data->name, data->argc, data->argv); 42 | return NULL; 43 | } 44 | 45 | static VALUE emitter_emit(VALUE self, VALUE name, VALUE args) { 46 | emitter_t *d; 47 | TypedData_Get_Struct(self, emitter_t, &data_type, d); 48 | 49 | int argc = RARRAY_LEN(args); 50 | const qmlbind_value **qmlArgs = malloc(argc * sizeof(qmlbind_value *)); 51 | 52 | for (int i = 0; i < argc; ++i) { 53 | qmlbind_value *value = rbqml_to_qml(RARRAY_AREF(args, i)); 54 | qmlArgs[i] = value; 55 | } 56 | 57 | emit_data data; 58 | data.emitter = d->emitter; 59 | data.name = rb_id2name(SYM2ID(name)); 60 | data.argc = argc; 61 | data.argv = qmlArgs; 62 | 63 | rb_thread_call_without_gvl(&emit_impl, &data, RUBY_UBF_IO, NULL); 64 | return self; 65 | } 66 | 67 | void rbqml_init_signal_emitter(void) { 68 | rbqml_cSignalEmitter = rb_define_class_under(rb_path2class("QML"), "SignalEmitter", rb_cObject); 69 | rb_define_alloc_func(rbqml_cSignalEmitter, &emitter_alloc); 70 | rb_define_method(rbqml_cSignalEmitter, "emit", &emitter_emit, 2); 71 | } 72 | -------------------------------------------------------------------------------- /ext/qml/signal_emitter.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "qml.h" 4 | 5 | extern VALUE rbqml_cSignalEmitter; 6 | 7 | VALUE rbqml_signal_emitter_new(qmlbind_signal_emitter *emitter); 8 | 9 | void rbqml_init_signal_emitter(void); 10 | -------------------------------------------------------------------------------- /lib/qml.rb: -------------------------------------------------------------------------------- 1 | require 'qml/version' 2 | require 'qml/errors' 3 | require 'qml/qml' 4 | 5 | require 'qml/core_ext' 6 | 7 | require 'qml/plugin_loader' 8 | require 'qml/plugins' 9 | require 'qml/component' 10 | require 'qml/engine' 11 | require 'qml/application' 12 | require 'qml/signal' 13 | require 'qml/reactive' 14 | require 'qml/access' 15 | require 'qml/qt' 16 | require 'qml/root_path' 17 | require 'qml/js_object' 18 | require 'qml/js_array' 19 | require 'qml/js_util' 20 | require 'qml/proc_access' 21 | 22 | require 'qml/data' 23 | -------------------------------------------------------------------------------- /lib/qml/access.rb: -------------------------------------------------------------------------------- 1 | module QML 2 | 3 | # {Access} enables classes to be exposed to QML. 4 | # 5 | module Access 6 | # @!parse include Reactive 7 | # @!parse extend ClassMethods 8 | 9 | def set_signal_emitter(emitter) 10 | self.class.signals.each do |name| 11 | __send__(name).connect do |*args| 12 | emitter.emit(name, args) 13 | end 14 | end 15 | end 16 | private :set_signal_emitter 17 | 18 | def self.included(derived) 19 | derived.class_eval do 20 | include Reactive 21 | extend ClassMethods 22 | end 23 | end 24 | 25 | def to_qml 26 | @qml_value ||= self.class.meta_class.wrap(self) 27 | end 28 | 29 | # allowed name patterns for exposed method names 30 | ALLOWED_PATTERN = /^[a-zA-Z_]\w*$/ 31 | 32 | module ClassMethods 33 | 34 | def meta_class 35 | @meta_class ||= begin 36 | meta_class = MetaClass.new(self, name) 37 | 38 | signals = self.signals.grep(ALLOWED_PATTERN) 39 | properties = self.properties.grep(ALLOWED_PATTERN) 40 | 41 | signals.each do |signal| 42 | meta_class.add_signal(signal, signal_infos[signal].params) 43 | end 44 | 45 | properties.each do |prop| 46 | meta_class.add_property(prop, :"#{prop}_changed") 47 | end 48 | 49 | methods = ancestors.take_while { |k| k.include?(Access) } 50 | .map { |k| k.instance_methods(false) }.inject(&:|) 51 | .grep(ALLOWED_PATTERN) 52 | ignored_methods = signals | properties.flat_map { |p| [p, :"#{p}=", :"#{p}_changed"] } 53 | 54 | (methods - ignored_methods).each do |method| 55 | instance_method = self.instance_method(method) 56 | # ignore variadic methods 57 | if instance_method.arity >= 0 58 | meta_class.add_method(method, instance_method.arity) 59 | end 60 | end 61 | meta_class 62 | end 63 | end 64 | 65 | # Registers the class as a QML type. 66 | # @param opts [Hash] 67 | # @option opts [String] :under the namespece which encapsulates the exported QML type. If not specified, automatically inferred from the module nesting of the class. 68 | # @option opts [String] :version the version of the type. Defaults to VERSION constant of the encapsulating module / class of the class. 69 | # @option opts [String] :name the name of the type. Defaults to the name of the class. 70 | def register_to_qml(opts = {}) 71 | QML.on_init do 72 | register_to_qml_impl(opts) 73 | end 74 | end 75 | 76 | def register_to_qml_impl(opts) 77 | metadata = guess_metadata(opts) 78 | 79 | meta_class.register( 80 | metadata[:under], 81 | metadata[:versions][0], 82 | metadata[:versions][1], 83 | metadata[:name] 84 | ) 85 | end 86 | 87 | private 88 | 89 | def guess_metadata(opts) 90 | under = opts[:under] 91 | version = opts[:version] 92 | name = opts[:name] 93 | 94 | if !under || !version || !name 95 | path = self.name.split('::') 96 | end 97 | if !under && !version 98 | fail AccessError, "cannot guess namespace of toplevel class '#{self.name}'" if path.size == 1 99 | encapsulatings = path[0, path.size - 1] 100 | end 101 | 102 | under ||= encapsulatings.join('.') 103 | version ||= eval("::#{encapsulatings.join('::')}").const_get(:VERSION) 104 | versions = version.split('.').map(&method(:Integer)) 105 | fail AccessError, 'insufficient version (major and minor versions required)' unless versions.size >= 2 106 | name ||= path.last 107 | 108 | {under: under, versions: versions, name: name} 109 | end 110 | end 111 | end 112 | end 113 | -------------------------------------------------------------------------------- /lib/qml/application.rb: -------------------------------------------------------------------------------- 1 | module QML 2 | # {Application} represents a Qt application instance. 3 | # It provides the event loop and manages Application-level configurations. 4 | # 5 | # @see http://qt-project.org/doc/qt-5/qapplication.html QApplication (C++) 6 | class Application 7 | 8 | # @return [Engine] The engine of the application. 9 | def engine 10 | QML.engine 11 | end 12 | 13 | # @return [Component] The root component of the application that represents the loaded QML file. 14 | # @see #load 15 | # @see #load_data 16 | # @see #load_path 17 | def root_component 18 | @root_component or fail ApplicationError, "QML data or file has not been loaded" 19 | end 20 | 21 | # Loads a QML file. The loaded component can be accessed by {#root_component} 22 | # @param [String] data 23 | # @param [String] path 24 | # @see Component 25 | def load(data: nil, path: nil) 26 | @root_component = Component.new(data: data, path: path) 27 | @root = @root_component.create 28 | end 29 | 30 | # Loads a QML file from string data. 31 | # @see #load 32 | def load_data(data) 33 | load(data: data) 34 | end 35 | 36 | # Loads a QML file from a file path. 37 | # @see #load 38 | def load_path(path) 39 | load(path: path) 40 | end 41 | 42 | # @return The root object created by the root component. 43 | def root 44 | @root or fail "QML data or file has not been loaded" 45 | end 46 | 47 | # Quits the application. 48 | def quit 49 | QML.qt.quit 50 | end 51 | end 52 | 53 | INIT_BLOCKS = [] 54 | 55 | module_function 56 | 57 | def on_init(&block) 58 | INIT_BLOCKS << block 59 | end 60 | 61 | # Initializes ruby-qml. 62 | # @param [Array] args Arguments to pass to the application 63 | def init(args = []) 64 | init_impl(args) 65 | end 66 | 67 | # Creates an {Application}, yields it and then call {QML::Application#exec}. 68 | # @return [Application] 69 | # @example 70 | # QML.run do |app| 71 | # app.load_path Pathname(__FILE__) + '../main.qml' 72 | # end 73 | def run 74 | QML.init 75 | QML.application.tap do |app| 76 | yield app 77 | app.exec 78 | end 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /lib/qml/component.rb: -------------------------------------------------------------------------------- 1 | require 'qml/engine' 2 | 3 | module QML 4 | # The Component class is used to instantiate objects like Window / ApplicationWindow objects from QML files. 5 | # 6 | # You usually do not need to use this class because Application#load, #load_path, #load_data do same 7 | # for the application top-level objects such as main windows. 8 | # @example 9 | # component = Component.new(engine: engine, path: path_to_qml_file) 10 | # root_object = component.create 11 | # @see http://qt-project.org/doc/qt-5/qqmlcomponent.html QQmlComponent (C++) 12 | class Component 13 | attr_reader :data, :path 14 | 15 | # Creates an component. Either data or path must be specified. 16 | # @param [String] data the QML file data. 17 | # @param [#to_s] path the QML file path. 18 | # @return QML::Component 19 | def initialize(data: nil, path: nil) 20 | fail TypeError, "neither data nor path privided" unless data || path 21 | 22 | initialize_impl 23 | 24 | case 25 | when data 26 | load_data(data) 27 | when path 28 | load_path(path) 29 | end 30 | end 31 | 32 | def load_path(path) 33 | path = path.to_s 34 | check_error_string do 35 | @path = Pathname.new(path) 36 | load_path_impl(path) 37 | end 38 | self 39 | end 40 | 41 | def load_data(data) 42 | check_error_string do 43 | @data = data 44 | load_data_impl(data, "<>") 45 | end 46 | self 47 | end 48 | 49 | # Instantiates a object from the QML file. 50 | # @return [QML::JSObject] The created object 51 | def create 52 | check_error_string do 53 | create_impl 54 | end 55 | end 56 | 57 | private 58 | 59 | def check_error_string 60 | yield.tap do 61 | fail QMLError, error_string if error_string 62 | end 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /lib/qml/core_ext.rb: -------------------------------------------------------------------------------- 1 | require 'qml/core_ext/to_qml' 2 | -------------------------------------------------------------------------------- /lib/qml/core_ext/to_qml.rb: -------------------------------------------------------------------------------- 1 | 2 | class Numeric 3 | def to_qml 4 | to_f 5 | end 6 | end 7 | 8 | class String 9 | def to_qml 10 | self 11 | end 12 | end 13 | 14 | class Symbol 15 | def to_qml 16 | to_s 17 | end 18 | end 19 | 20 | class TrueClass 21 | def to_qml 22 | self 23 | end 24 | end 25 | 26 | class FalseClass 27 | def to_qml 28 | self 29 | end 30 | end 31 | 32 | class NilClass 33 | def to_qml 34 | self 35 | end 36 | end 37 | 38 | class Array 39 | def to_qml 40 | QML.engine.new_array(self.size).tap do |jsarray| 41 | self.each_with_index do |x, i| 42 | jsarray[i] = x 43 | end 44 | end 45 | end 46 | end 47 | 48 | class Hash 49 | def to_qml 50 | QML.engine.new_object.tap do |jsobj| 51 | self.each do |key, value| 52 | jsobj[key] = value 53 | end 54 | end 55 | end 56 | end 57 | 58 | class Time 59 | def to_qml 60 | QML::JSUtil.classes['Date'].new(year, month, day, hour, min, sec, nsec / 1000000).tap do |date| 61 | date.setTime((to_r * 1000).floor) 62 | end 63 | end 64 | end 65 | 66 | class Proc 67 | def to_qml 68 | QML::ProcAccess.wrap_proc(self) 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /lib/qml/data.rb: -------------------------------------------------------------------------------- 1 | require 'qml/data/list_model_access' 2 | require 'qml/data/list_model' 3 | require 'qml/data/array_model' 4 | require 'qml/data/query_model' 5 | -------------------------------------------------------------------------------- /lib/qml/data/array_model.rb: -------------------------------------------------------------------------------- 1 | module QML 2 | # {ArrayModel} is one of ruby-qml's list models and it stores data in Array simply. 3 | class ArrayModel < ListModel 4 | 5 | # @param [Array] columns 6 | def initialize(*columns) 7 | super 8 | @array = [] 9 | end 10 | 11 | # Duplicates the internal array and returns it. 12 | # @return [Array] 13 | def to_a 14 | @array.dup 15 | end 16 | 17 | # @return [Integer] the number of the items. 18 | def count 19 | @array.count 20 | end 21 | 22 | # Returns an item. 23 | # @param [Integer] index the index of the item. 24 | # @return the item. 25 | def [](index) 26 | @array[index] 27 | end 28 | 29 | # Updates an item. 30 | # @param [Integer] index 31 | # @param item 32 | # @return the item. 33 | def []=(index, item) 34 | @array[index] = item 35 | update(index .. index) 36 | item 37 | end 38 | 39 | # Inserts items. 40 | # @param [Integer] index 41 | # @return [self] 42 | def insert(index, *items) 43 | inserting(index ... index + items.size) do 44 | @array.insert(index, *items) 45 | end 46 | self 47 | end 48 | 49 | # @overload delete_at(index) 50 | # Deletes an item. 51 | # @param [Integer] index 52 | # @return the deleted item. 53 | # @overload delete_at(index, count) 54 | # Deletes items. 55 | # @param [Integer] index 56 | # @return [Array] the deleted items. 57 | def delete_at(index, count = nil) 58 | if count 59 | removing(index ... index + count) do 60 | count.times.map { @array.delete_at(index) } 61 | end 62 | else 63 | removing(index .. index) do 64 | @array.delete_at(index) 65 | end 66 | end 67 | end 68 | 69 | # Prepend items. 70 | # @return [self] 71 | def unshift(*items) 72 | insert(0, *items) 73 | end 74 | 75 | # @overload shift 76 | # Deletes the first item. 77 | # @return the deleted item. 78 | # @overload shift(count) 79 | # Deletes the first items. 80 | # @return [Array] the deleted items. 81 | def shift(count = nil) 82 | delete_at(0, count) 83 | end 84 | 85 | # Append items. 86 | # @return [self] 87 | def push(*items) 88 | insert(@array.size, *items) 89 | end 90 | 91 | alias_method :<<, :push 92 | 93 | # @overload pop 94 | # Deletes the last item. 95 | # @return the deleted item. 96 | # @overload pop(count) 97 | # Deletes the last items. 98 | # @return [Array] the deleted items. 99 | def pop(count = nil) 100 | delete_at(@array.size - count, count) 101 | end 102 | 103 | # Deletes all items. 104 | # @return [self] 105 | def clear 106 | removing(0 ... count) do 107 | @array.clear 108 | end 109 | self 110 | end 111 | 112 | # Replaces entire array with given array. 113 | # @param [Array] ary 114 | # @return [self] 115 | def replace(ary) 116 | resetting do 117 | @array = ary.dup 118 | end 119 | self 120 | end 121 | end 122 | end 123 | -------------------------------------------------------------------------------- /lib/qml/data/list_model.rb: -------------------------------------------------------------------------------- 1 | module QML 2 | # {ListModel} is the base class of list models which provides data to QML list views. 3 | # @see http://qt-project.org/doc/qt-5/qabstractitemmodel.html QAbstractItemModel (C++) 4 | # @see http://qt-project.org/doc/qt-5/qabstractlistmodel.html QAbstractListModel (C++) 5 | class ListModel 6 | include Enumerable 7 | 8 | # @return [Array] 9 | attr_reader :columns 10 | 11 | # @param [Array] columns the column names of the model. 12 | def initialize(*columns) 13 | @columns = columns 14 | @access = ListModelAccess.create(self) 15 | @qml_model = QML::Plugins.rubyqml.createListModel(@access) 16 | end 17 | 18 | # Iterates each item. 19 | # @overload each 20 | # @return [Enumerator] 21 | # @overload each 22 | # @yield [item] 23 | # @return [self] 24 | def each 25 | return to_enum unless block_given? 26 | count.times do |i| 27 | yield self[i] 28 | end 29 | self 30 | end 31 | 32 | # @abstract 33 | # @return [Integer] the number of the items. 34 | def count 35 | fail ::NotImplementedError 36 | end 37 | 38 | # Returns an item. 39 | # @abstract 40 | # @param [Integer] index the index of the item. 41 | # @return the item. 42 | def [](index) 43 | fail ::NotImplementedError 44 | end 45 | 46 | # @return [QML::JSObject] 47 | def to_qml 48 | @qml_model 49 | end 50 | 51 | protected 52 | 53 | # Notifies the list views that the data of the items was changed. 54 | # @param [Range] range the index range of changed items. 55 | def update(range) 56 | @access.update(range.min, range.max) 57 | end 58 | 59 | # Notifies the list views that items are about to be and were moved. 60 | # @param [Range] range the index range of the item being moved. 61 | # @param [Integer] destination the first index of the items after moved. 62 | # @yield the block that actually do moving operation of the items. 63 | # @return the result of given block. 64 | # @see http://qt-project.org/doc/qt-5/qabstractitemmodel.html#beginMoveRows QAbstractItemModel::beginMoveRows 65 | # @see http://qt-project.org/doc/qt-5/qabstractitemmodel.html#endMoveRows QAbstractItemModel::endMoveRows 66 | # @see #inserting 67 | # @see #removing 68 | def moving(range, destination) 69 | return if range.count == 0 70 | 71 | @access.begin_move(range.min, range.max, destination) 72 | ret = yield 73 | @access.end_move 74 | ret 75 | end 76 | 77 | # Notifies the list views that items are about to be and were inserted. 78 | # @param [Range] range the index range of the items after inserted. 79 | # @yield the block that actually do insertion of the items. 80 | # @return the result of give block. 81 | # @example 82 | # inserting(index ... index + items.size) do 83 | # @array.insert(index, *items) 84 | # end 85 | # @see http://qt-project.org/doc/qt-5/qabstractitemmodel.html#beginInsertRows QAbstractItemModel::beginInsertRows 86 | # @see http://qt-project.org/doc/qt-5/qabstractitemmodel.html#endInsertRows QAbstractItemModel::endInsertRows 87 | # @see #removing 88 | # @see #moving 89 | def inserting(range, &block) 90 | return if range.count == 0 91 | 92 | @access.begin_insert(range.min, range.max) 93 | ret = yield 94 | @access.end_insert 95 | ret 96 | end 97 | 98 | # Notifies the list views that items are about to be and were removed. 99 | # @param [Range] range the index range of the items before removed. 100 | # @yield the block that actually do removal of the items. 101 | # @return the result of give block. 102 | # @see http://qt-project.org/doc/qt-5/qabstractitemmodel.html#beginRemoveRows QAbstractItemModel::beginRemoveRows 103 | # @see http://qt-project.org/doc/qt-5/qabstractitemmodel.html#endRemoveRows QAbstractItemModel::endRemoveRows 104 | # @see #inserting 105 | # @see #moving 106 | def removing(range, &block) 107 | return if range.count == 0 108 | 109 | @access.begin_remove(range.min, range.max) 110 | ret = yield 111 | @access.end_remove 112 | ret 113 | end 114 | 115 | def resetting(&block) 116 | @access.begin_reset 117 | ret = yield 118 | @access.end_reset 119 | ret 120 | end 121 | end 122 | end 123 | -------------------------------------------------------------------------------- /lib/qml/data/list_model_access.rb: -------------------------------------------------------------------------------- 1 | module QML 2 | # @api private 3 | class ListModelAccess 4 | include Access 5 | 6 | signal :begin_insert, [:first, :last] 7 | signal :end_insert 8 | signal :begin_move, [:first, :last, :dest] 9 | signal :end_move 10 | signal :begin_remove, [:first, :last] 11 | signal :end_remove 12 | signal :update, [:first, :last] 13 | signal :begin_reset 14 | signal :end_reset 15 | 16 | attr_accessor :model 17 | 18 | def columns 19 | @model.columns.to_qml 20 | end 21 | 22 | def data(index, column) 23 | @model[index.to_i][column.to_sym].to_qml 24 | end 25 | 26 | def count 27 | @model.count 28 | end 29 | 30 | register_to_qml under: 'RubyQml', version: QML::VERSION 31 | 32 | # @return [QML::JSWrapper] 33 | def self.create(model) 34 | @component ||= QML::Component.new(data: <<-QML) 35 | import RubyQml 1.0 36 | ListModelAccess {} 37 | QML 38 | 39 | @component.create.tap do |access| 40 | access.unwrap.model = model 41 | end 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/qml/data/query_model.rb: -------------------------------------------------------------------------------- 1 | module QML 2 | # {QueryModel} provides a list model implementation with database backends like ActiveRecord. 3 | class QueryModel < ListModel 4 | attr_reader :count 5 | 6 | # @param [Array] columns 7 | def initialize(*columns) 8 | super 9 | @count = 0 10 | @caches = [] 11 | update 12 | end 13 | 14 | def [](index) 15 | block_index = index / CACHE_SIZE 16 | cache = @caches.find { |c| c.block_index == block_index } || add_cache(block_index) 17 | cache.items[index % CACHE_SIZE] 18 | end 19 | 20 | # Updates the model. 21 | def update 22 | @caches = [] 23 | resetting do 24 | @count = query_count 25 | end 26 | end 27 | 28 | # @abstract 29 | # Queries the count of the records. 30 | # Called when {#update} is called and the result is set as the {#count} of the model. 31 | # @return [Integer] 32 | def query_count 33 | fail ::NotImplementedError 34 | end 35 | 36 | # @abstract 37 | # Queries a block of records. The results are chached. 38 | # @param [Integer] offset 39 | # @param [Integer] count 40 | # @return [Array] 41 | def query(offset, count) 42 | fail ::NotImplementedError 43 | end 44 | 45 | private 46 | 47 | Cache = Struct.new(:block_index, :items) 48 | CACHE_SIZE = 256 49 | CACHE_COUNT = 4 50 | 51 | def add_cache(block_offset) 52 | @caches.shift if @caches.size >= CACHE_COUNT 53 | Cache.new(block_offset, query(block_offset * CACHE_SIZE, CACHE_SIZE)).tap do |cache| 54 | @caches << cache 55 | end 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /lib/qml/engine.rb: -------------------------------------------------------------------------------- 1 | module QML 2 | # @!parse class Engine < QtObjectBase; end 3 | 4 | # {Engine} provides a QML engine. 5 | # 6 | # @see http://qt-project.org/doc/qt-5/qqmlengine.html QQmlEngine (C++) 7 | class Engine 8 | 9 | # Evaluates an JavaScript expression 10 | # @param [String] str The JavaScript string 11 | # @param [String] file The file name 12 | # @param [Integer] lineno The line number 13 | def evaluate(str, file = '', lineno = 1) 14 | evaluate_impl(str, file, lineno).tap do |result| 15 | raise result.to_error if result.is_a?(JSObject) && result.error? 16 | end 17 | end 18 | 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/qml/errors.rb: -------------------------------------------------------------------------------- 1 | module QML 2 | class PluginError < StandardError; end 3 | class QMLError < StandardError; end 4 | class AccessError < StandardError; end 5 | end 6 | -------------------------------------------------------------------------------- /lib/qml/interface.rb: -------------------------------------------------------------------------------- 1 | module QML 2 | module Interface 3 | 4 | def self.call_method(obj, name, args) 5 | begin 6 | obj.__send__ name, *args 7 | rescue => error 8 | notify_error(error) 9 | nil 10 | end 11 | end 12 | 13 | # Called when an Ruby error is occured in executing Qt code. 14 | # @param error The error (or the exception) 15 | def self.notify_error(error) 16 | warn "-- An error occured when running Ruby code from Qt --" 17 | warn "#{error.class.name}: #{error.message}" 18 | warn "Backtrace: \n\t#{error.backtrace.join("\n\t")}" 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/qml/js_array.rb: -------------------------------------------------------------------------------- 1 | module QML 2 | class JSArray < JSObject 3 | # @return [Array] 4 | def to_a 5 | each.to_a 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/qml/js_object.rb: -------------------------------------------------------------------------------- 1 | module QML 2 | # The JSObject represents JavaScript objects. 3 | class JSObject 4 | 5 | alias_method :each, :each_pair 6 | 7 | # @return [Array] 8 | def keys 9 | each.map { |k, v| k } 10 | end 11 | 12 | # @return [Array] 13 | def values 14 | each.map { |k, v| v } 15 | end 16 | 17 | # @return [Hash] 18 | def to_hash 19 | {}.tap do |hash| 20 | each do |k, v| 21 | hash[k] =v 22 | end 23 | end 24 | end 25 | 26 | # @return [Time] 27 | def to_time 28 | Time.at(getTime.to_i / 1000r).getlocal(-getTimezoneOffset * 60) 29 | end 30 | 31 | # @return [QML::QMLError] 32 | def to_error 33 | QMLError.new(self['message']) 34 | end 35 | 36 | def respond_to?(method) 37 | has_key?(method) || super 38 | end 39 | 40 | # Gets or sets a JS property, or call it as a method if it is a function. 41 | def method_missing(method, *args, &block) 42 | if method[-1] == '=' 43 | # setter 44 | key = method.slice(0...-1).to_sym 45 | 46 | unless has_key?(key) 47 | super 48 | end 49 | self[key] = args[0] 50 | else 51 | unless has_key?(method) 52 | super 53 | end 54 | 55 | prop = self[method] 56 | if prop.is_a? JSFunction 57 | prop.call_with_instance(self, *args, &block) 58 | else 59 | prop 60 | end 61 | end 62 | end 63 | 64 | # @return [QML::JSObject] self 65 | def to_qml 66 | self 67 | end 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /lib/qml/js_util.rb: -------------------------------------------------------------------------------- 1 | module QML 2 | module JSUtil 3 | 4 | def self.data 5 | @data ||= QML.engine.evaluate <<-JS 6 | ({ 7 | classes: { 8 | Date: Date 9 | } 10 | }) 11 | JS 12 | end 13 | 14 | def self.classes 15 | data.classes 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/qml/name_helper.rb: -------------------------------------------------------------------------------- 1 | module QML 2 | module NameHelper 3 | module_function 4 | 5 | def to_underscore(sym) 6 | sym.to_s.gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').gsub(/([a-z\d])([A-Z])/,'\1_\2').downcase.to_sym 7 | end 8 | def to_upper_underscore(sym) 9 | to_underscore(sym).upcase 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/qml/platform.rb: -------------------------------------------------------------------------------- 1 | module QML 2 | 3 | module Platform 4 | module_function 5 | 6 | def windows? 7 | !!(/cygwin|mswin|mingw|bccwin|wince|emx/ =~ RUBY_PLATFORM) 8 | end 9 | 10 | def mac? 11 | !!(/darwin/ =~ RUBY_PLATFORM) 12 | end 13 | end 14 | 15 | end -------------------------------------------------------------------------------- /lib/qml/plugin_loader.rb: -------------------------------------------------------------------------------- 1 | require 'pathname' 2 | require 'qml/platform' 3 | 4 | module QML 5 | # {PluginLoader} loads Qt C++ plugins and enables you to use your Qt C++ codes from Ruby easily. 6 | # @see http://qt-project.org/doc/qt-5/qpluginloader.html QPluginLoader (C++) 7 | class PluginLoader 8 | 9 | # @overload initialize(path) 10 | # @param [String|Pathname] path the library path (may be platform-dependent). 11 | # @example 12 | # loader = QML::PluginLoader.new('path/to/libhoge.dylib') 13 | # @overload initialize(dir, libname) 14 | # @param [String|Pathname] dir the library directory. 15 | # @param [String] libname the platform-independent library name. 16 | # @example 17 | # loader = QML::PluginLoader.new('path/to', 'hoge') 18 | def initialize(path, libname = nil) 19 | path = Pathname(path) + self.class.lib_filename(libname) if libname 20 | initialize_impl(path.to_s) 21 | end 22 | 23 | def self.lib_filename(libname) 24 | case 25 | when Platform::windows? 26 | "#{libname}.dll" 27 | when Platform::mac? 28 | "lib#{libname}.dylib" 29 | else 30 | "lib#{libname}.so" 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/qml/plugins.rb: -------------------------------------------------------------------------------- 1 | require 'pathname' 2 | 3 | module QML 4 | # @api private 5 | module Plugins 6 | class << self 7 | def rubyqml 8 | @plugin ||= QML::PluginLoader.new(ROOT_PATH + "ext/qml/rubyqml-plugin", "rubyqml-plugin").instance 9 | end 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/qml/proc_access.rb: -------------------------------------------------------------------------------- 1 | module QML 2 | class ProcAccess 3 | include Access 4 | register_to_qml under: 'RubyQml', version: QML::VERSION 5 | 6 | def call(args) 7 | @proc.call(*args) 8 | end 9 | 10 | # @return [QML::JSFunction] 11 | def self.wrap_proc(prc) 12 | @bind_call ||= QML.engine.evaluate <<-JS 13 | (function (access) { return access.call.bind(access); }) 14 | JS 15 | 16 | @component ||= QML::Component.new(data: <<-QML) 17 | import RubyQml 1.0 18 | ProcAccess {} 19 | QML 20 | 21 | access = @component.create 22 | access.unwrap.instance_eval do 23 | @proc = prc 24 | end 25 | @bind_call.call(access) 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/qml/qml.rb: -------------------------------------------------------------------------------- 1 | require_relative '../../ext/qml/qml' 2 | -------------------------------------------------------------------------------- /lib/qml/qt.rb: -------------------------------------------------------------------------------- 1 | 2 | module QML 3 | module_function 4 | 5 | # @example 6 | # def set_app_name 7 | # QML.qt.application.name = 'appname' 8 | # end 9 | # @return [JSObject] QML Qt namespace object 10 | # @see http://doc.qt.io/qt-5/qml-qtqml-qt.html 11 | def qt 12 | @qt ||= begin 13 | component = QML::Component.new data: <<-QML 14 | import QtQuick 2.0 15 | QtObject { 16 | function getQt() { 17 | return Qt; 18 | } 19 | } 20 | QML 21 | component.create.getQt 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/qml/reactive.rb: -------------------------------------------------------------------------------- 1 | module QML 2 | 3 | module Reactive 4 | # @!parse extend ClassMethods 5 | 6 | # @api private 7 | class PropertyInfo 8 | attr_accessor :initializer 9 | end 10 | 11 | # @api private 12 | class SignalInfo 13 | attr_accessor :params 14 | attr_accessor :listeners 15 | 16 | def initialize 17 | @listeners = [] 18 | end 19 | end 20 | 21 | def initialize(*args, &block) 22 | 23 | self.class.property_infos.each do |name, info| 24 | if info.initializer 25 | self.__send__ :"#{name}=", instance_eval(&info.initializer) 26 | end 27 | end 28 | 29 | self.class.signal_infos.each do |name, info| 30 | __send__(name).connect do |*args| 31 | info.listeners.each do |listener| 32 | instance_exec(*args, &listener) 33 | end 34 | end 35 | end 36 | 37 | super 38 | end 39 | 40 | def self.included(derived) 41 | derived.class_eval do 42 | extend ClassMethods 43 | end 44 | end 45 | 46 | module ClassMethods 47 | 48 | # @api public 49 | # Defines a signal. 50 | # @example 51 | # class Button 52 | # include QML::Reactive::Object 53 | # signal :pressed, [:pos] 54 | # ... 55 | # end 56 | def signal(name, params = []) 57 | name = name.to_sym 58 | signal_infos(false)[name] = SignalInfo.new.tap do |info| 59 | info.params = params.map(&:to_sym) 60 | end 61 | 62 | class_eval <<-EOS, __FILE__, __LINE__ + 1 63 | def #{name} 64 | @_signal_#{name} ||= begin 65 | args = self.class.signal_infos[:#{name}].params 66 | Signal.new(args) 67 | end 68 | end 69 | EOS 70 | 71 | name 72 | end 73 | 74 | private :signal 75 | 76 | def signals(include_super = true) 77 | signal_infos(include_super).keys 78 | end 79 | 80 | def signal_infos(include_super = true) 81 | if include_super && superclass.include?(Access) 82 | superclass.signal_infos.merge(signal_infos(false)) 83 | else 84 | @signal_infos ||= {} 85 | end 86 | end 87 | 88 | # Defines a property. 89 | # @example 90 | # class Foo 91 | # include QML::Access 92 | # property(:name) { 'hogehoge' } 93 | # ... 94 | # end 95 | def property(name, &block) 96 | name = name.to_sym 97 | signal(:"#{name}_changed", [:"new_#{name}"]) 98 | 99 | property_infos(false)[name] = PropertyInfo.new.tap do |info| 100 | info.initializer = block 101 | end 102 | 103 | class_eval <<-EOS, __FILE__, __LINE__ + 1 104 | attr_reader :#{name} 105 | def #{name}=(new_value) 106 | new_value = new_value 107 | if @#{name} != new_value 108 | @#{name} = new_value 109 | #{name}_changed.emit(new_value) 110 | end 111 | end 112 | EOS 113 | 114 | name 115 | end 116 | 117 | private :property 118 | 119 | def properties(include_super = true) 120 | property_infos(include_super).keys 121 | end 122 | 123 | def property_infos(include_super = true) 124 | if include_super && superclass.include?(Access) 125 | superclass.property_infos.merge(property_infos(false)) 126 | else 127 | @property_infos ||= {} 128 | end 129 | end 130 | 131 | def on(signal, &block) 132 | info = signal_infos(false)[signal.to_sym] or fail AccessError, "no signal `#{signal}` found" 133 | info.listeners << block 134 | block 135 | end 136 | 137 | def on_changed(property, &block) 138 | on(:"#{property}_changed", &block) 139 | end 140 | 141 | private :on, :on_changed 142 | 143 | end 144 | 145 | end 146 | 147 | end 148 | -------------------------------------------------------------------------------- /lib/qml/root_path.rb: -------------------------------------------------------------------------------- 1 | module QML 2 | ROOT_PATH = Pathname.new(__FILE__) + '../../..' 3 | end 4 | -------------------------------------------------------------------------------- /lib/qml/signal.rb: -------------------------------------------------------------------------------- 1 | module QML 2 | 3 | class Signal 4 | 5 | class Connection 6 | def initialize(signal, listener) 7 | @signal = signal 8 | @listener = listener 9 | end 10 | 11 | # Disconnects the connection. 12 | def disconnect 13 | @signal.disconnect(@listener) 14 | end 15 | end 16 | 17 | attr_reader :arity 18 | 19 | # Initializes the Signal. 20 | # @param [Array<#to_sym>, nil] params the parameter names (the signal will be variadic if nil). 21 | def initialize(params) 22 | @listeners = [] 23 | @params = params.map(&:to_sym) 24 | @arity = params.size 25 | end 26 | 27 | # Calls every connected procedure with given arguments. 28 | # Raises an ArgumentError when the arity is wrong. 29 | # @param args the arguments. 30 | def emit(*args) 31 | if args.size != @arity 32 | fail ::ArgumentError ,"wrong number of arguments for signal (#{args.size} for #{@arity})" 33 | end 34 | @listeners.each do |listener| 35 | listener.call(*args) 36 | end 37 | end 38 | 39 | # Returns the format of the parameters in the same format as Proc#parameters. 40 | # @return [Array>] 41 | def parameters 42 | @params ? @params.map { |arg| [:req, arg] } : [[:rest, :args]] 43 | end 44 | 45 | # Returns the number of connections. 46 | # @return [Integer] 47 | def connection_count 48 | @listeners.size 49 | end 50 | 51 | # Connects a procedure. 52 | # @yield called when #emit is called. 53 | # @return [QML::Signal::Connection] 54 | def connect(&listener) 55 | @listeners << listener 56 | Connection.new(self, listener) 57 | end 58 | 59 | # Disconnects a procedure. 60 | # @param listener 61 | # @return [self] 62 | def disconnect(listener) 63 | @listeners.delete(listener) 64 | self 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /lib/qml/version.rb: -------------------------------------------------------------------------------- 1 | module QML 2 | VERSION = '1.0.2' 3 | end 4 | -------------------------------------------------------------------------------- /qml.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'qml/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "qml" 8 | spec.version = QML::VERSION 9 | spec.authors = ["Ryohei Ikegami"] 10 | spec.email = ["iofg2100@gmail.com"] 11 | spec.summary = %q{A QML / Qt Quick wrapper for Ruby} 12 | spec.description = "ruby-qml provides bindings between QML and Ruby and enables you to use Qt Quick-based GUI from Ruby." 13 | spec.homepage = "http://seanchas116.github.io/ruby-qml/" 14 | spec.license = "MIT" 15 | 16 | qmlbind_dir = "ext/qml/lib/libqmlbind" 17 | qmlbind_files = Dir.chdir(qmlbind_dir) do 18 | `git ls-files -z` 19 | .split("\x0") 20 | .map { |file| "#{qmlbind_dir}/#{file}" } 21 | end 22 | 23 | spec.files = `git ls-files -z`.split("\x0") + qmlbind_files 24 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 25 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 26 | spec.require_paths = ["lib"] 27 | spec.extensions = ["ext/qml/extconf.rb"] 28 | 29 | spec.required_ruby_version = '>= 1.9.3' 30 | 31 | spec.add_development_dependency "bundler", "~> 1.5" 32 | spec.add_development_dependency "rake", "~> 10.3" 33 | spec.add_development_dependency "rspec", "~> 3.0" 34 | spec.add_development_dependency 'pry' 35 | spec.add_development_dependency 'twitter', '~> 5.11' 36 | spec.add_development_dependency 'sequel', '~> 4.12' 37 | spec.add_development_dependency 'sqlite3', '~> 1.3' 38 | end 39 | -------------------------------------------------------------------------------- /spec/assets/test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seanchas116/ruby-qml/569af4c31460d2946c18346dc94751c77b1d4dfc/spec/assets/test.png -------------------------------------------------------------------------------- /spec/assets/testmodule/qmldir: -------------------------------------------------------------------------------- 1 | module testmodule 2 | Test 1.0 test.qml 3 | -------------------------------------------------------------------------------- /spec/assets/testmodule/test.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | 3 | QtObject { 4 | property string name: 'poyo' 5 | } 6 | -------------------------------------------------------------------------------- /spec/assets/testobj.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | 3 | QtObject { 4 | property string name: 'foo' 5 | } 6 | -------------------------------------------------------------------------------- /spec/qml/access_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe QML::Access do 4 | 5 | let(:component) { QML::Component.new data: data } 6 | let(:root) { component.create } 7 | 8 | describe '.register_to_qml' do 9 | 10 | context 'when namespace, version, name are given' do 11 | 12 | let(:data) do 13 | <<-EOS 14 | import QtQuick 2.2 15 | import AccessExampleNS 1.2 16 | Item { 17 | id: root 18 | property var bound: example.text + example.text 19 | function getExample() { 20 | return example; 21 | } 22 | function callSomeMethod() { 23 | return example.some_method(100, 200); 24 | } 25 | AccessExample { 26 | id: example 27 | property var signalArg 28 | onSome_signal: { 29 | signalArg = arg; 30 | } 31 | } 32 | } 33 | EOS 34 | end 35 | let(:example) { root.getExample } 36 | 37 | it 'registers the class as a QML type' do 38 | expect { component.create }.not_to raise_error 39 | end 40 | describe 'AccessExamle#some_method' do 41 | it 'returns value' do 42 | expect(root.callSomeMethod()).to eq 300 43 | end 44 | end 45 | describe 'AccessExamle text property' do 46 | it 'can be used to property binding' do 47 | example.text = "foo" 48 | expect(root.bound).to eq 'foofoo' 49 | example.text = "bar" 50 | expect(root.bound).to eq 'barbar' 51 | end 52 | end 53 | describe 'AccessExamle some_signal signal' do 54 | it 'can be connected' do 55 | example.unwrap.some_signal.emit('foo') 56 | expect(example.signalArg).to eq 'foo' 57 | end 58 | end 59 | end 60 | 61 | context 'when arguments are omitted' do 62 | 63 | let(:data) do 64 | <<-EOS 65 | import AccessExampleModule 0.1 66 | AccessExample {} 67 | EOS 68 | end 69 | 70 | it 'guesses them from the Ruby class name, namespace and VERSION constant' do 71 | expect { component.create }.not_to raise_error 72 | end 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /spec/qml/application_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe QML::Application do 4 | let(:application) { QML.application } 5 | 6 | describe '#engine' do 7 | it 'returns the default engine of the application' do 8 | expect(application.engine).to be_a(QML::Engine) 9 | end 10 | end 11 | describe '#load_data' do 12 | let(:data) do 13 | <<-EOS 14 | import QtQuick 2.0 15 | QtObject { 16 | property string name: 'foo' 17 | } 18 | EOS 19 | end 20 | it 'loads root object 1from data' do 21 | application.load_data data 22 | expect(application.root_component.data).to eq data 23 | expect(application.root.name).to eq 'foo' 24 | end 25 | end 26 | describe '#load_path' do 27 | let(:path) { QML::ROOT_PATH + 'spec/assets/testobj.qml' } 28 | it 'loads root object from path' do 29 | application.load_path path 30 | expect(application.root_component.path).to eq path 31 | expect(application.root.name).to eq 'foo' 32 | end 33 | end 34 | end 35 | 36 | describe QML do 37 | describe '.application' do 38 | it 'returns the QML::Application instance' do 39 | expect(QML.application).to be_a(QML::Application) 40 | end 41 | end 42 | 43 | describe '.next_tick' do 44 | it 'do a task later in event loop' do 45 | finished = false 46 | QML.next_tick do 47 | finished = true 48 | end 49 | QML.application.process_events 50 | expect(finished).to eq true 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /spec/qml/component_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe QML::Component do 4 | 5 | describe '#create' do 6 | 7 | context 'with string' do 8 | let(:data) do 9 | <<-EOS 10 | import QtQuick 2.0 11 | QtObject { 12 | property string name: 'foo' 13 | } 14 | EOS 15 | end 16 | let(:component) { QML::Component.new(data: data) } 17 | 18 | it 'instantiates a object' do 19 | expect(component.create.name).to eq 'foo' 20 | end 21 | 22 | describe '#data' do 23 | it 'returns its data' do 24 | expect(component.data).to eq data 25 | end 26 | end 27 | end 28 | 29 | context 'with file path' do 30 | let(:path) { QML::ROOT_PATH + 'spec/assets/testobj.qml' } 31 | let(:component) { QML::Component.new(path: path) } 32 | 33 | it 'instantiates a object' do 34 | expect(component.create.name).to eq 'foo' 35 | end 36 | 37 | describe '#path' do 38 | it 'returns its path' do 39 | expect(component.path).to eq path 40 | end 41 | end 42 | end 43 | end 44 | 45 | describe '#initialize' do 46 | context 'when neither string nor path specified' do 47 | it 'fails with TypeError' do 48 | expect { QML::Component.new }.to raise_error(TypeError) 49 | end 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /spec/qml/data/array_model_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe QML::ArrayModel do 4 | 5 | class TestArrayModel < QML::ArrayModel 6 | def initialize 7 | super(:title, :number) 8 | end 9 | end 10 | 11 | let(:original_array) do 12 | [ 13 | {title: 'hoge', number: 12}, 14 | {title: 'piyo', number: 34}, 15 | {title: 'fuga', number: 1234} 16 | ] 17 | end 18 | 19 | let(:additional_array) do 20 | [ 21 | {title: 'foo', number: 56}, 22 | {title: 'bar', number: 78} 23 | ] 24 | end 25 | 26 | let(:expected_array) { original_array.dup } 27 | 28 | let(:model) do 29 | TestArrayModel.new.tap do |model| 30 | model.push(*original_array) 31 | end 32 | end 33 | 34 | include_context 'ListView for model available' 35 | 36 | shared_examples 'same as expected array' do |text| 37 | it text do 38 | expect(model.to_a).to eq(expected_array) 39 | end 40 | end 41 | 42 | describe '#each' do 43 | context 'with no block' do 44 | it 'returns an Enumerator' do 45 | expect(model.each).to be_a Enumerator 46 | expect(model.each.to_a).to eq expected_array 47 | end 48 | end 49 | 50 | context 'with block' do 51 | it 'iterates each item' do 52 | items = [] 53 | model.each do |item| 54 | items << item 55 | end 56 | expect(items).to eq expected_array 57 | end 58 | end 59 | end 60 | 61 | describe '#to_a' do 62 | it 'returns the array the elements are stored in' do 63 | expect(model.to_a).to eq expected_array 64 | end 65 | end 66 | 67 | describe '#count' do 68 | it 'returns the number of items' do 69 | expect(model.count).to eq expected_array.size 70 | end 71 | end 72 | 73 | describe '#[]' do 74 | it 'returns the item for given index' do 75 | expect(model[1]).to eq(expected_array[1]) 76 | end 77 | end 78 | 79 | describe '#[]=' do 80 | 81 | it 'returns the assigned item' do 82 | item = additional_array[0] 83 | expect(model[2] = item).to eq item 84 | end 85 | 86 | context 'after called' do 87 | before do 88 | model[2] = additional_array[0] 89 | expected_array[2] = additional_array[0] 90 | end 91 | it_behaves_like 'same as expected array', 'sets the element to given index' 92 | it_behaves_like 'ListView data source' 93 | end 94 | end 95 | 96 | describe '#insert' do 97 | 98 | it 'returns self' do 99 | expect(model.insert(1, *additional_array)).to eq model 100 | end 101 | 102 | context 'after called' do 103 | before do 104 | model.insert(1, *additional_array) 105 | expected_array.insert(1, *additional_array) 106 | end 107 | it_behaves_like 'same as expected array', 'inserts item' 108 | it_behaves_like 'ListView data source' 109 | end 110 | end 111 | 112 | describe '#delete_at' do 113 | 114 | context 'with no number' do 115 | it 'deletes 1 item and returns it' do 116 | expect(model.delete_at(1)).to eq original_array[1] 117 | end 118 | context 'after called' do 119 | before do 120 | model.delete_at(1) 121 | expected_array.delete_at(1) 122 | end 123 | it_behaves_like 'same as expected array', 'deletes item' 124 | it_behaves_like 'ListView data source' 125 | end 126 | end 127 | 128 | context 'with number' do 129 | it 'deletes multiple items and returns them as an array' do 130 | expect(model.delete_at(1, 2)).to eq original_array[1..2] 131 | end 132 | context 'after called' do 133 | before do 134 | model.delete_at(1, 2) 135 | 2.times { expected_array.delete_at(1) } 136 | end 137 | it_behaves_like 'same as expected array', 'deletes items' 138 | it_behaves_like 'ListView data source' 139 | end 140 | end 141 | end 142 | 143 | describe '#unshift' do 144 | it 'prepends items' do 145 | expect(model.unshift(*additional_array).to_a).to eq expected_array.unshift(*additional_array) 146 | end 147 | end 148 | 149 | describe '#shift' do 150 | it 'removes first items and returns them' do 151 | expect(model.shift(2)).to eq expected_array[0..1] 152 | expect(model.to_a).to eq expected_array.tap { |a| a.shift(2) } 153 | end 154 | end 155 | 156 | describe '#push' do 157 | it 'appends items' do 158 | expect(model.push(*additional_array).to_a).to eq expected_array.push(*additional_array) 159 | end 160 | end 161 | 162 | describe '#pop' do 163 | it 'removes last items and returns them' do 164 | expect(model.pop(2)).to eq expected_array[-2..-1] 165 | expect(model.to_a).to eq expected_array.tap { |a| a.pop(2) } 166 | end 167 | end 168 | 169 | describe '#<<' do 170 | it 'is an alias of #push' do 171 | expect((model << additional_array[0]).to_a).to eq(expected_array << additional_array[0]) 172 | end 173 | end 174 | 175 | describe '#clear' do 176 | it 'returns self' do 177 | expect(model.clear).to be(model) 178 | end 179 | context 'after called' do 180 | before do 181 | model.clear 182 | expected_array.clear 183 | end 184 | it_behaves_like 'same as expected array', 'clears items' 185 | it_behaves_like 'ListView data source' 186 | end 187 | end 188 | 189 | describe '#replace' do 190 | it 'returns self' do 191 | expect(model.replace(expected_array + additional_array)).to be(model) 192 | end 193 | context 'after called' do 194 | before do 195 | model.replace(expected_array + additional_array) 196 | expected_array.push *additional_array 197 | end 198 | it_behaves_like 'same as expected array', 'replaces entire array' 199 | it_behaves_like 'ListView data source' 200 | end 201 | end 202 | end 203 | -------------------------------------------------------------------------------- /spec/qml/data/list_model_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe QML::ListModel do 4 | let(:model) { QML::ListModel.allocate } 5 | 6 | describe '#count' do 7 | it 'fails with NotImplementedError by default' do 8 | expect { model.count }.to raise_error(NotImplementedError) 9 | end 10 | end 11 | 12 | describe '#[]' do 13 | it 'fails with NotImplementedError by default' do 14 | expect { model[0] }.to raise_error(NotImplementedError) 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /spec/qml/data/query_model_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe QML::QueryModel do 4 | 5 | let(:klass) do 6 | Class.new(QML::QueryModel) do 7 | attr_accessor :data 8 | 9 | def initialize(count) 10 | @data = count.times.map { |i| {title: "title: #{i}", number: i} } 11 | super(:title, :number) 12 | end 13 | 14 | def query(offset, count) 15 | @data[offset ... offset + count] 16 | end 17 | 18 | def query_count 19 | @data.size 20 | end 21 | end 22 | end 23 | 24 | let(:model) { klass.new(2000) } 25 | let(:expected_array) { model.data } 26 | 27 | describe '#count' do 28 | it 'returns the count and updated by #update' do 29 | count = model.data.size 30 | expect(model.count).to eq(count) 31 | model.data << {value: 0} 32 | expect(model.count).to eq(count) 33 | model.update 34 | expect(model.count).to eq(count + 1) 35 | end 36 | end 37 | 38 | describe '#[]' do 39 | it 'returns the item' do 40 | model.data.size.times do |i| 41 | expect(model[i]).to eq(model.data[i]) 42 | end 43 | end 44 | end 45 | 46 | describe '#query_count' do 47 | it 'fails with NotImplementedError by default' do 48 | expect { QML::QueryModel.allocate.query_count }.to raise_error(NotImplementedError) 49 | end 50 | end 51 | 52 | describe '#query' do 53 | it 'fails with NotImplementedError by default' do 54 | expect { QML::QueryModel.allocate.query(0, 100) }.to raise_error(NotImplementedError) 55 | end 56 | end 57 | 58 | include_context 'ListView for model available' 59 | it_behaves_like 'ListView data source' do 60 | let(:model) { klass.new(10) } 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /spec/qml/engine_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe QML::Engine do 4 | 5 | describe '#add_import_path' do 6 | context 'with test module' do 7 | let(:path) { QML::ROOT_PATH + 'spec/assets' } 8 | 9 | before do 10 | QML.engine.add_import_path(path) 11 | end 12 | 13 | let(:data) do 14 | <<-EOS 15 | import QtQuick 2.0 16 | import testmodule 1.0 17 | Test {} 18 | EOS 19 | end 20 | let(:component) { QML::Component.new(data: data) } 21 | 22 | it 'loads a module' do 23 | expect(component.create.name).to eq 'poyo' 24 | end 25 | end 26 | end 27 | 28 | describe '#evaluate' do 29 | it 'evaluates JS scripts' do 30 | result = QML.engine.evaluate <<-JS 31 | (function() { 32 | return "foo"; 33 | })(); 34 | JS 35 | expect(result).to eq('foo') 36 | end 37 | context 'with error' do 38 | it 'fails with QMLError' do 39 | block = proc do 40 | QML.engine.evaluate <<-JS 41 | (function() { 42 | throw new Error("hoge"); 43 | })(); 44 | JS 45 | end 46 | expect(&block).to raise_error(/hoge/) 47 | end 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /spec/qml/interface_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe QML::Interface do 4 | 5 | describe '.notify_error' do 6 | let(:error) do 7 | begin 8 | fail "hoge" 9 | rescue => e 10 | e 11 | end 12 | end 13 | 14 | it 'prints an error to stderr' do 15 | expect { QML::Interface.notify_error(error) } 16 | .to output(/#{error.message}/).to_stderr 17 | end 18 | end 19 | 20 | end 21 | -------------------------------------------------------------------------------- /spec/qml/js_array_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe QML::JSArray do 4 | 5 | let(:array_script) do 6 | <<-JS 7 | [1, 2, 3] 8 | JS 9 | end 10 | let(:array) { QML.engine.evaluate(array_script) } 11 | 12 | describe '#each' do 13 | it 'enumerates each values' do 14 | expect(array.each.to_a).to eq [1,2,3] 15 | end 16 | end 17 | 18 | describe '#to_a' do 19 | it 'converts it to an array' do 20 | expect(array.to_a).to eq [1,2,3] 21 | end 22 | end 23 | 24 | describe '#length' do 25 | it 'returns length' do 26 | expect(array.length).to eq 3 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /spec/qml/js_function_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe QML::JSFunction do 4 | 5 | let(:function) do 6 | QML.engine.evaluate <<-JS 7 | (function(a, b) { 8 | return a + b; 9 | }); 10 | JS 11 | end 12 | 13 | let(:constructor) do 14 | QML.engine.evaluate <<-JS 15 | (function(a, b) { 16 | this.value = a + b; 17 | }); 18 | JS 19 | end 20 | 21 | describe '#call' do 22 | it 'calls function' do 23 | expect(function.call(1,2)).to eq 3 24 | end 25 | end 26 | 27 | describe '#new' do 28 | it 'calls function as a constructor' do 29 | expect(constructor.new(1, 2).value).to eq 3 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /spec/qml/js_object_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe QML::JSObject do 4 | 5 | let(:obj_script) do 6 | <<-JS 7 | ({one: 1, two: 2}) 8 | JS 9 | end 10 | let(:obj) { QML.engine.evaluate(obj_script) } 11 | 12 | describe '#[]' do 13 | it 'get property' do 14 | expect(obj['one']).to eq(1) 15 | expect(obj['two']).to eq(2) 16 | end 17 | end 18 | 19 | describe '#each' do 20 | it 'enumerates each item' do 21 | expect(obj.each.to_a).to eq [["one", 1], ["two", 2]] 22 | end 23 | end 24 | 25 | describe '#each_pair' do 26 | it 'enumerates each item' do 27 | expect(obj.each_pair.to_a).to eq [["one", 1], ["two", 2]] 28 | end 29 | end 30 | 31 | describe '#to_hash' do 32 | it 'converts it to a Hash' do 33 | expect(obj.to_hash).to eq({"one" => 1, "two" => 2}) 34 | end 35 | end 36 | 37 | describe '#keys' do 38 | it 'returns all keys' do 39 | expect(obj.keys).to eq %w{one two} 40 | end 41 | end 42 | 43 | describe '#values' do 44 | it 'returns all values' do 45 | expect(obj.values).to eq [1, 2] 46 | end 47 | end 48 | 49 | describe '#has_key?' do 50 | it 'returns whether it has the key' do 51 | expect(obj.has_key?(:one)).to eq true 52 | expect(obj.has_key?(:hoge)).to eq false 53 | end 54 | end 55 | 56 | describe '#respond_to?' do 57 | it 'returns whether it has the key method' do 58 | expect(obj.respond_to?(:one)).to eq true 59 | expect(obj.respond_to?(:hoge)).to eq false 60 | expect(obj.respond_to?(:respond_to?)).to eq true 61 | end 62 | end 63 | 64 | describe '#==' do 65 | let(:obj_script) do 66 | <<-JS 67 | ({ 68 | obj: {} 69 | }) 70 | JS 71 | end 72 | 73 | it 'returns identity' do 74 | expect(obj.obj).to eq(obj.obj) 75 | end 76 | end 77 | 78 | describe 'method call' do 79 | 80 | let(:obj_script) do 81 | <<-JS 82 | ({ 83 | one: 1, 84 | addOne: function(x) { 85 | return x + this.one; 86 | } 87 | }) 88 | JS 89 | end 90 | 91 | context 'for non-function property' do 92 | it 'get it' do 93 | expect(obj.one).to eq 1 94 | end 95 | end 96 | context 'for function' do 97 | it 'call it as method' do 98 | expect(obj.addOne(2)).to eq 3 99 | end 100 | end 101 | context 'for non-existing key' do 102 | it 'fails with NoMethodError' do 103 | expect { obj.hoge }.to raise_error(NoMethodError) 104 | end 105 | end 106 | context 'as setter' do 107 | it 'assigns value' do 108 | obj.one = 2 109 | expect(obj.one).to eq 2 110 | end 111 | end 112 | end 113 | end 114 | -------------------------------------------------------------------------------- /spec/qml/plugin_loader_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe QML::PluginLoader do 4 | 5 | let(:loader) { QML::PluginLoader.new path, 'rubyqml-plugin' } 6 | 7 | describe '#instance' do 8 | 9 | context 'with signle path' do 10 | let(:path) { QML::ROOT_PATH + 'ext/qml/rubyqml-plugin' + QML::PluginLoader.lib_filename('rubyqml-plugin') } 11 | let(:loader) { QML::PluginLoader.new path } 12 | it 'returns an object instance' do 13 | expect(loader.instance).to respond_to('createListModel') 14 | end 15 | end 16 | 17 | context 'with correct file path' do 18 | let(:path) { QML::ROOT_PATH + 'ext/qml/rubyqml-plugin' } 19 | it 'returns an object instance' do 20 | expect(loader.instance).to respond_to('createListModel') 21 | end 22 | end 23 | 24 | context 'with wrong file path' do 25 | let(:path) { QML::ROOT_PATH + 'ext/qml/plugins/wrong' } 26 | it 'fails with QML::PluginError' do 27 | expect { loader.instance }.to raise_error(QML::PluginError) 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /spec/qml/qt_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe QML do 4 | describe '.qt' do 5 | it 'returns the Qt object' do 6 | expect(QML.qt.md5('hoge')).to eq('ea703e7aa1efda0064eaa507d9e8ab7e') 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/qml/reactive_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe QML::Reactive do 4 | let(:button_class) do 5 | Class.new do 6 | include QML::Reactive 7 | signal :pressed 8 | property(:is_pressed) { false } 9 | property :id 10 | property(:is_id_changed) { false } 11 | 12 | on :pressed do |pos| 13 | self.is_pressed = true 14 | end 15 | on_changed :id do 16 | self.is_id_changed = true 17 | end 18 | end 19 | end 20 | let(:button) { button_class.new } 21 | 22 | describe '.on' do 23 | it 'registers a signal listener' do 24 | expect(button.is_pressed).to eq(false) 25 | button.pressed.emit 26 | expect(button.is_pressed).to eq(true) 27 | end 28 | end 29 | 30 | describe '.on_changed' do 31 | it 'registers a property change listener' do 32 | expect(button.is_id_changed).to eq(false) 33 | button.id = "foo" 34 | expect(button.is_id_changed).to eq(true) 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /spec/qml/signal_connect_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "QML signal connection" do 4 | 5 | let(:component) do 6 | QML::Component.new(data: <<-QML) 7 | import QtQuick 2.0 8 | QtObject { 9 | signal someSignal(var arg) 10 | } 11 | QML 12 | end 13 | 14 | let(:obj) { component.create } 15 | 16 | it "connects QML signals to ruby proc" do 17 | received_arg = nil 18 | 19 | obj[:someSignal].connect do |arg| 20 | received_arg = arg 21 | end 22 | 23 | obj.someSignal(10) 24 | 25 | expect(received_arg).to eq(10) 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/qml/signal_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe QML::Signal do 4 | 5 | before do 6 | @signal = QML::Signal.new([:foo, :bar]) 7 | end 8 | 9 | describe 'connection' do 10 | 11 | describe '#connect, #emit' do 12 | 13 | before do 14 | @signal.connect { |foo, bar| @received = [foo, bar] } 15 | end 16 | 17 | it 'connects a block and calls it' do 18 | @signal.emit('apple', 'orange') 19 | expect(@received).to eq(['apple', 'orange']) 20 | end 21 | 22 | context 'when the number of arguments is wrong' do 23 | it 'fails with an ArgumentError' do 24 | expect { @signal.emit('apple') }.to raise_error(ArgumentError) 25 | end 26 | end 27 | 28 | it 'calls connected blocks in the order they have been conncted' do 29 | received = [] 30 | @signal.connect { received << 'first' } 31 | @signal.connect { received << 'second' } 32 | @signal.emit('foo', 'bar') 33 | expect(received).to eq(['first', 'second']) 34 | end 35 | end 36 | 37 | describe '::Connection#disconnect' do 38 | 39 | it 'disconnects the connection' do 40 | connection = @signal.connect { |foo, bar| @received = foo + bar } 41 | @signal.emit('apple', 'orange') 42 | connection.disconnect 43 | @signal.emit('banana', 'grape') 44 | expect(@received).to eq('appleorange') 45 | end 46 | end 47 | 48 | describe '#connection_count' do 49 | it 'returns the number of connections' do 50 | prc1 = ->(foo, bar) {} 51 | prc2 = ->(foo, bar) {} 52 | @signal.connect(&prc1) 53 | @signal.connect(&prc2) 54 | expect(@signal.connection_count).to eq(2) 55 | end 56 | end 57 | end 58 | 59 | describe 'attributes' do 60 | 61 | describe '#arity' do 62 | it 'returns the number of arguments' do 63 | expect(@signal.arity).to eq(2) 64 | end 65 | end 66 | 67 | describe '#parameters' do 68 | it 'returns the parameter information' do 69 | expect(@signal.parameters).to eq([[:req, :foo], [:req, :bar]]) 70 | end 71 | end 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /spec/qml/to_qml_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe '#to_qml' do 4 | 5 | let(:through) { QML.engine.evaluate("(function(x) { return x; })") } 6 | 7 | primitives = { 8 | Fixnum: 10, 9 | Float: 1.5, 10 | Rational: 5/2r, 11 | TrueClass: true, 12 | FalseClass: false, 13 | String: 'ほげ' 14 | } 15 | 16 | primitives.each do |type, value| 17 | describe "#{type}\#to_qml" do 18 | it "converts #{type} to JS correspondings" do 19 | expect(through.call(value)).to eq value 20 | end 21 | end 22 | end 23 | 24 | describe 'Symbol#to_qml' do 25 | it 'converts Symbol to JS string' do 26 | expect(through.call(:hoge)).to eq 'hoge' 27 | end 28 | end 29 | 30 | describe 'Hash#to_qml' do 31 | it 'converts hash to JS object' do 32 | hash = {a: 1, b: 2} 33 | obj = through.call(hash); 34 | expect(obj).to be_a QML::JSObject 35 | hash.each do |k, v| 36 | expect(obj[k]).to eq v 37 | end 38 | end 39 | end 40 | 41 | describe 'Array#to_qml' do 42 | it 'converts array to JS array' do 43 | array = [1,2,3] 44 | jsarray = through.call(array) 45 | expect(jsarray).to be_a QML::JSArray 46 | expect(jsarray.each.to_a).to eq array 47 | end 48 | end 49 | 50 | describe 'Time#to_qml' do 51 | it 'converts time to JS Date' do 52 | time = Time.now 53 | # millisecond precision 54 | time -= (time.nsec % 1000000) / 1000000000r 55 | jsarray = through.call(time) 56 | expect(jsarray.to_time).to eq time 57 | end 58 | end 59 | 60 | describe 'Access#to_qml' do 61 | it 'converts access to QML value' do 62 | access = AccessExample.new 63 | obj = through.call(access) 64 | expect(obj.some_method(1, 2)).to eq(3) 65 | end 66 | end 67 | 68 | describe 'Proc#to_qml' do 69 | it 'converts Proc into JS function' do 70 | proc = -> (hoge) { hoge * 2 } 71 | func = through.call(proc) 72 | expect(func).to be_a(QML::JSFunction) 73 | expect(func.call(2)).to eq(4) 74 | end 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /spec/qml_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe QML do 4 | it 'should have a version number' do 5 | expect(QML::VERSION).not_to be_nil 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/shared/qml/access_example.rb: -------------------------------------------------------------------------------- 1 | 2 | class AccessExample 3 | include QML::Access 4 | 5 | property :text 6 | signal :some_signal, [:arg] 7 | 8 | def some_method(a, b) 9 | a + b 10 | end 11 | 12 | register_to_qml under: 'AccessExampleNS', version: '1.2', name: 'AccessExample' 13 | end 14 | 15 | module AccessExampleModule 16 | VERSION = '0.1.0' 17 | class AccessExample 18 | include QML::Access 19 | register_to_qml 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/shared/qml/data/list_model.rb: -------------------------------------------------------------------------------- 1 | shared_context 'ListView for model available' do 2 | let(:component) do 3 | QML::Component.new data: <<-EOS 4 | import QtQuick 2.0 5 | ListView { 6 | model: ListModel {} 7 | delegate: Item { 8 | property var itemTitle: title 9 | property var itemNumber: number 10 | } 11 | } 12 | EOS 13 | end 14 | let(:list_view) { component.create } 15 | end 16 | 17 | shared_examples 'ListView data source' do 18 | it 'updates ListView correctly' do 19 | list_view.model = model 20 | 21 | count = list_view.count.to_i 22 | 23 | expect(count).to eq expected_array.size 24 | 25 | count.times do |i| 26 | list_view.currentIndex = i 27 | current = list_view.currentItem 28 | expect(current.itemTitle).to eq(expected_array[i][:title]) 29 | expect(current.itemNumber).to eq(expected_array[i][:number]) 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'coveralls' 2 | Coveralls.wear! 3 | 4 | $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) 5 | require 'qml' 6 | 7 | Dir.glob(Pathname(__FILE__) +'../shared/**/*.rb') do |f| 8 | require f 9 | end 10 | 11 | QML.init(%w{-platform offscreen}) 12 | --------------------------------------------------------------------------------