status-desktop/docs/qml_nim_communication.md

7.6 KiB

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:

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

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:

proc foo*(self: MyView): string {.slot.} =
    "hello world"

and in QML doing

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:

...
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:

...
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:

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:

...
    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

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

  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:

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

Let's take as an example src/app/wallet/views/asset_list.nim

First the imports

import NimQml
import tables

then we define the QtObject macro as usual but this time the object uses QAbstractListModel

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

type Asset* = ref object
    name*, symbol*, value*, fiatValue*, accountAddress*, address*: string

then there is the typical required initialization

  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

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:

  # 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:

    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