status-desktop/docs/qml_nim_communication.md

267 lines
7.6 KiB
Markdown

## Communicating with NIM
**Using NimQml - General Overview**
Nim objects meant to be exposed to QT import `NimQml` and use the `QtObject` macro, there is some basic methods that need to be setup for every `QtObject` such as `setup`, `delete`, and when initializing the object the `new` and `setup` method need to be called.
A basic QtObject will look like something like:
```nimrod=
import NimQml
QtObject:
type MyView* = ref object of QObject
someField*: string
proc setup(self: MyView) =
self.QObject.setup
proc delete*(self: MyView) =
self.QObject.delete
proc newMyView*(): MyView =
new(result, delete)
result = MyView()
result.setup
```
The object then is exposed to QML by creating and registering a `QVariant`
```nimrod=
import NimQml
...
# create variant
var view = newMyView()
var variant: QVariant = newQVariant(view)
# expose it to QML
let engine = newQQmlApplicationEngine()
engine.setRootContextProperty("MyNimObject", variant)
```
The variable `MyNimObject` is then accessible in QML and represent `MyView` and its methods or variables that have been defined to be exposed to QML, for example, adding:
```qml
proc foo*(self: MyView): string {.slot.} =
"hello world"
```
and in QML doing
```qml
Text {
text: "NIM says" + MyNimObject.foo()
}
```
will create a text "NIM says hello world"
**NimQml in nim-status-client**
The QtObjects are defined in `src/app/<module>/view.nim` and `src/app/<module>/views/`, they typically include the nim-status object as a parameter, for example `src/app/profile/view.nim`:
```nimrod=
...
QtObject:
type ProfileView* = ref object of QObject
...
status*: Status
proc newProfileView*(status: Status): ProfileView =
new(result, delete)
result = ProfileView()
...
result.status = status
result.setup
...
```
The variant is created and wrapped in the "controller" `src/app/<module>/core.nim`, for example `src/app/profile/core.nim`:
```nimrod=
...
type ProfileController* = ref object of SignalSubscriber
view*: ProfileView
variant*: QVariant
status*: Status
proc newController*(status: Status): ProfileController =
result = ProfileController()
result.status = status
result.view = newProfileView(status)
result.variant = newQVariant(result.view)
```
This controller is initialized in `src/nim_status_client.nim` and the variant is registered there, for example:
```nimrod=
var profile = profile.newController(status)
engine.setRootContextProperty("profileModel", profile.variant)
```
this variant is then accessible in QML as `profileModel`, for example in `ui/app/AppLayouts/Profile/Sections/AboutContainer.qml` the node version is displayed with:
```qml
...
StyledText {
text: qsTr("Node Version: %1").arg(profileModel.nodeVersion())
...
}
...
```
**exposing methods to QML**
Methods can be exposed to QML need to be public and use the `{.slot.}` pragma
```nimrod=
QtObject:
...
proc nodeVersion*(self: ProfileView): string {.slot.} =
self.status.getNodeVersion()
```
**QtProperty Macro for simple types**
There is a handy `QtProperty[type]` macro, this macro defines what methods to call to get the latest value (`read`), which method updates that value (`write`) and a signal that notifies that value has changed (`notify`), here is a real example from `src/app/wallet/view.nim` that defines the `defaultCurrency` property
```nimrod=
proc defaultCurrency*(self: WalletView): string {.slot.} =
self.status.wallet.getDefaultCurrency()
proc defaultCurrencyChanged*(self: WalletView) {.signal.}
proc setDefaultCurrency*(self: WalletView, currency: string) {.slot.} =
self.status.wallet.setDefaultCurrency(currency)
self.defaultCurrencyChanged() # notify value has changed
QtProperty[string] defaultCurrency:
read = defaultCurrency
write = setDefaultCurrency
notify = defaultCurrencyChange
```
note: it's not necessary to define all these fields except for `read`
**QtProperty Macro for other QObjects**
This macro can also be used to expose other QtObjects as QVariants, this is typically done to simplify code and sometimes even required for things that need to be their own individual QTObjects such as Lists.
For example, in `src/app/profile/view.nim` the profileView QtObject (`src/app/profile/profileView.nim`) is exposed to QML with:
```nimrod=
QtObject:
type ProfileView* = ref object of QObject
profile*: ProfileInfoView
...
...
proc getProfile(self: ProfileView): QVariant {.slot.} =
return newQVariant(self.profile)
proc setNewProfile*(self: ProfileView, profile: Profile) =
self.profile.setProfile(profile)
QtProperty[QVariant] profile:
read = getProfile
```
**QAbstractListModel**
Lists are exposed to QML using a `QAbstractListModel` object, this method expects certain methods to be defined so QML can access the data: `rowCount`, `data` and `roleNames`
Other methods can be found in the QT documentation [here](https://doc.qt.io/qt-5/qabstractitemmodel.html)
Let's take as an example `src/app/wallet/views/asset_list.nim`
First the imports
```nim
import NimQml
import tables
```
then we define the `QtObject` macro as usual but this time the object uses `QAbstractListModel`
```nim
QtObject:
type AssetList* = ref object of QAbstractListModel
assets*: seq[Asset]
```
`assets` is the sequence that will hold the assets, `Asset` is imported and defined in `src/status/wallet/accounts.nim` and is a simple nim object
```nimrod=
type Asset* = ref object
name*, symbol*, value*, fiatValue*, accountAddress*, address*: string
```
then there is the typical required initialization
```nimrod=
proc setup(self: AssetList) = self.QAbstractListModel.setup
proc delete(self: AssetList) =
self.QAbstractListModel.delete
self.assets = @[]
proc newAssetList*(): AssetList =
new(result, delete)
result.assets = @[]
result.setup
```
a role enum type needs to be defined, specifying the name of each field
```nimrod=
type
AssetRoles {.pure.} = enum
Name = UserRole + 1,
Symbol = UserRole + 2,
Value = UserRole + 3,
FiatValue = UserRole +
```
for the data to be exposed there are methods that need to be defined such as `rowCount` and `data`:
```nimrod=
# returns total assets
method rowCount(self: AssetList, index: QModelIndex = nil): int =
return self.assets.len
# returns Asset object at given index
method data(self: AssetList, index: QModelIndex, role: int): QVariant =
if not index.isValid:
return
if index.row < 0 or index.row >= self.assets.len:
return
let asset = self.assets[index.row]
let assetRole = role.AssetRoles
case assetRole:
of AssetRoles.Name: result = newQVariant(asset.name)
of AssetRoles.Symbol: result = newQVariant(asset.symbol)
of AssetRoles.Value: result = newQVariant(asset.value)
of AssetRoles.FiatValue: result = newQVariant(asset.fiatValue)
# returns table with columns names and values
method roleNames(self: AssetList): Table[int, string] =
{ AssetRoles.Name.int:"name",
AssetRoles.Symbol.int:"symbol",
AssetRoles.Value.int:"value",
AssetRoles.FiatValue.int:"fiatValue" }.toTable
```
The asset list has been exposed in `src/app/wallet/view.nim` as QVariant called `assets` and the table can be display in QML, for example using a `ListView`:
```qml
ListView {
model: walletModel.assets // the table
delegate: Text {
text: "name:" + name + " | symbol: " + symbol
}
}
```
**TODO**: qml components with default properties
**TODO**: reusable qml components
**TODO**: qml components alias properties