add docs and tutorials

add docs and tutorials

add test index

update readme

update readme
This commit is contained in:
Iuri Matias 2020-06-22 09:25:24 -04:00
parent 7e96a17f3b
commit 0e02b8aaee
9 changed files with 1030 additions and 0 deletions

18
docs/README.md Normal file
View File

@ -0,0 +1,18 @@
# Nim-Status-Client Developer Documentation, Tutorials & Guides
*note: this documentation is wip*
## Overview
* [QML Crash Course](qml_crash_course.md)
* [NimQml](qml_nim_communication.md)
## Architecture & Development
* [Project Architecture & Structure](structure.md)
* [Common development errors aand how to solve them](common_errors.md)
* [tips & tricks](tips.md)
## Guides & Tutorials
* [Tutorial - how to add a new section](tutorial_adding_section.md)
* [Tutorial - how to create a custom QML component](tutorial_custom_component.md)
## API
* [QML Nim-Status-Client API reference](qml_api.md)

91
docs/common_errors.md Normal file
View File

@ -0,0 +1,91 @@
## Common errors and how to solve them
### SIGSEGV: Illegal storage access. (Attempt to read from nil?)
This happens due to using a null pointer, it can be caused by several situations:
**calling status-go with invalid parameters**
Calling status-go with a json that is missing a field somewhere can cause status-go to crash somewhere or throw an exception that is not being caught
**listening for non existing events**
If an event in which a corresponding `emit` does not exist, it can cause this error
```nimrod=
events.on("event-does-not-exist") do(a: Args):
appState.addChannel("test")
appState.addChannel("test2")
```
**parsing json**
when working with json, this error could be triggered for several reasons
* accessing a property that doesn't exist
* get the value type that doesn't match the value in the json
* `if payload["contentType"].str == 2:` will crash because the value of contentType is `2` not `"2"`
* something extracting a string with `value.str` instead of `$value` (sometimes `.getStr`)
### Error: attempting to call undeclared routine
this happens due something missing in the QTObject, it's caused for when a proc is not marked as a slot, not being public, not part of a variant, missing the self attribute or not mapped in a qproperty if it is an accesor
*TODO: add practical examples*
### Unsupported conversion of X to metatype
this can happen due to a method being exposed to QT as a slot but using an object (like a model X) that is not a QtObject.
possible solutions:
- make the object a QtObject (usually only recommended if it's in the view only)
- remove the {.slot.} pragma if it's not being called from QML anyway
- change the method to receive params individually and then build the model inside the method
### typeless parameters are obsolete
typically means types are missing for a method parameters
### attempting to call undeclared routine
routine is likely not public or is being declared after the method calling it
### QML Invalid component body specification
This error happens when a `Component` has multiple children, it must only contain one child, to fix it, put the component children inside a `Item {}`
### QML TypeError: Property 'setFunctionName' of object SomeView(0x7fa4bf55b240) is not a function
Likely the function is missing a `{.slot.}` pragma
### QML input text value not being updated
If you are using an `Input` QML prop, to get the current value use `idName.TextField.text` instead of `idName.text`
### QMutex: destroying locked mutex
a common scenario this error can happen is when trying to immediatly access something in status-go when the app starts before the node is ready. it can also happen due to 2 threads attempting to call & change something from status-go at the same time
## Warnings
### QML anchor warnings
Those look like
```
Cannot specify top, bottom, verticalCenter, fill or centerIn anchors for items inside Column. Column will not function.
```
or
```
Detected anchors on an item that is managed by a layout. This is undefined behavior; use Layout.alignment instead.
```
Those mean that you used anchors on an element that is manged by a Layout. Those are ColumnLayouts, StackLayouts, etc.
The first child of anything in a "Something"Layout will not have access to anchors (they will throw warnings).
First thing to ask yourself, do you really need a Layout? That's the easiest way to fix it. Unless you really need your block to be a row or a column that needs to go next/under another, use an Item or similar. Usually, you can still do the same effect anyway with anchors on the siblings
If you really need the Layout, then one way to fix is to set the first child of the Layout an `Item` and then every other child inside the `Item`. That way, all the children can use anchors. You can set
```
Layout.fillHeight: true
Layout.fillWidth: true
```
on the `Item` to make it fill the whole parent so that nothing else needs to be changed.

140
docs/qml_api.md Normal file
View File

@ -0,0 +1,140 @@
## API available to QML
**walletModel**
*walletModel.currentAccount* - returns current account (object)
*walletModel.currentAccount.name* -
*walletModel.currentAccount.address* -
*walletModel.currentAccount.iconColor* -
*walletModel.currentAccount.balance* -
*walletModel.currentAccount.path* -
*walletModel.currentAccount.walletType* -
*walletModel.transactions* - list of transactions (list)
each transaction is an object containing:
* typeValue
* address
* blockNumber
* blockHash
* timestamp
* gasPrice
* gasLimit
* gasUsed
* nonce
* txStatus
* value
* fromAddress
* to
*walletModel.assets* - list of assets (list)
each list is an object containing:
* name
* symbol
* value
* fiatValue
*walletModel.totalFiatBalance* - returns total fiat balance of all accounts (string)
*walletModel.accounts* - list of accounts (list)
each account is an object containing:
* name
* address
* iconColor
* balance
*walletModel.defaultCurrency* - get current currency (string)
*walletModel.setDefaultCurrency(currency: string)* - set a new default currency, `currency` should be a symbol like `"USD"`
*walletModel.hasAsset(account: string, symbol: string)* - returns true if token with `symbol` is enabled, false other wise (boolean)
*walletModel.toggleAsset(symbol: string, checked: bool, address: string, name: string, decimals: int, color: string)* - enables a token with `symbol` or disables it it's already enabled
*walletModel.addCustomToken(address: string, name: string, symbol: string, decimals: string)* - add a custom token to the wallet
*walletModel.loadTransactionsForAccount(address: string)* - loads transaction history for an address
*walletModel.onSendTransaction(from_value: string, to: string, value: string, password: string)* - transfer a value in ether from one account to another
*walletModel.deleteAccount(address: string)* - delete an address from the wallet
*generateNewAccount(password: string, accountName: string, color: string)* -
*addAccountsFromSeed(seed: string, password: string, accountName: string, color: string)* -
*addAccountsFromPrivateKey(privateKey: string, password: string, accountName: string, color: string)* -
*addWatchOnlyAccount(address: string, accountName: string, color: string)* -
*changeAccountSettings(address: string, accountName: string, color: string)* -
**chatsModel**
*chatsModel.chats* - get channel list (list)
channel object:
* name -
* timestamp -
* lastMessage.text -
* unviewedMessagesCount -
* identicon -
* chatType -
* color -
*chatsModel.activeChannelIndex* -
*chatsModel.activeChannel* - return currently active channel (object)
active channel object:
* id -
* name -
* color -
* identicon -
* chatType - (int)
* members - (list)
* userName
* pubKey
* isAdmin
* joined
* identicon
* isMember(pubKey: string) - check if `pubkey` is a group member (bool)
* isAdmin(pubKey: string) - check if `pubkey` is a group admin (bool)
*chatsModel.messageList* - returns messages for the current channel (list)
message object:
* userName -
* message -
* timestamp -
* clock -
* identicon -
* isCurrentUser -
* contentType -
* sticker -
* fromAuthor -
* chatId -
* sectionIdentifier -
* messageId -
*chatsModel.sendMessage(message: string)* - send a message to currently active channel
*chatsModel.joinChat(channel: string, chatTypeInt: int)* - join a channel
*chatsModel.joinGroup()* - confirm joining group
*chatsModel.leaveActiveChat()* - leave currently active channel
*chatsModel.clearChatHistory()* - clear chat history of currently active channel
*chatsModel.renameGroup(newName: string)* - rename current active group
*chatsModel.blockContact(id: string)* - block contact
*chatsModel.addContact(id: string)*
*chatsModel.createGroup(groupName: string, pubKeys: string)*
**TODO**: document all exposed APIs to QML

168
docs/qml_crash_course.md Normal file
View File

@ -0,0 +1,168 @@
### QML Crash course
**Intro**
Every QML file imports at least QtQuick and then other imports that might be required for the QML Types being used. QML Types have properties (similar to CSS somewhat), and typically contain other QML Types as children.
```qml
import QtQuick 2.0
import <SomeImportNeededForTypeName>
TypeName {
propertyName: value
AnotherType {
propertyName: value
anotherProperty: value2
}
}
```
example:
```qml
import QtQuick 2.0
Rectangle {
id: page
width: 320; height: 480
color: "lightgray"
Text {
id: helloText
text: "Hello world!"
y: 30
anchors.horizontalCenter: page.horizontalCenter
font.pointSize: 24; font.bold: true
}
}
```
**QML Properties - using ids**
QML Types can be identified by an `id` which can be used as variable to access other properties from that element as parameter to other elements.
In this example, the `Text` element is identified with the id `tabBtnText`, we can use this to access the width of the text and used as value for the width of the Rectangle so its width is always the same as the text:
```qml
Rectangle {
id: tabButton
width: tabBtnText.width // will always reflect the width of tabBtnText
height: tabBtnText.height + 11
Text {
id: tabBtnText
text: "hello there"
}
}
```
Another example, combining a `TabBar` and a `StackLayout`, the StackLayout will display a different view depending on which TabButton has been selected since its index is taking the value of the tabbar
```qml
TabBar {
id: tabBar
currentIndex: 0
TabButton { ... } // will change currentIndex to 0 if selected
TabButton { ... } // will change currentIndex to 1 if selected
...
}
StackLayout {
...
currentIndex: tabBar.currentIndex // use the newest value of the TabBar
Item {} // will be displayed if currentIndex == 0
item {} // will be displayed if currentIndex == 1
...
}
```
**QML Properties - parent and children**
It's possible to also refer to a `parent` of an element. This is typically used for widths & anchors but can be used to access any property from the parent, for example:
```qml
ColumnLayout {
id: suggestionsContainer
Layout.fillHeight: true
Layout.fillWidth: true
Row {
id: description
anchors.right: parent.right
anchors.rightMargin: 20
anchors.left: parent.left
anchors.leftMargin: 20
width: parent.width
}
}
```
Or even a particular child using `children`, for example, here the rectangle remains at the width of the child text with an additional room of 10 pixels:
```qml
Rectangle {
width: children[0].width + 10
Text {
text: "#" + channel
}
}
```
**QML Types**
A complete list of QML Types can be found in the QT documentation [here](https://doc.qt.io/qt-5/qmltypes.html)
some commonly used types in nim-status-client include:
* [Text](https://doc.qt.io/qt-5/qml-qtquick-text.html)
* [Image](https://doc.qt.io/qt-5/qml-qtquick-image.html)
* SplitView
* TabBar & TabButton
* StackLayout
* ColumnLayout & RowLayout
* ListView
**SplitView Example**
The SplitView list items with a draggable splitter between each item
```qml
import QtQuick 2.0
import QtQuick.Controls 2.13 // required for SplitView
SplitView {
id: walletView
// splitter settings
handleDelegate: Rectangle {
implicitWidth: 1
implicitHeight: 4
color: Theme.grey
}
Text {
text: "item on the left"
}
Text {
text: "item on the right"
}
}
```
**TabBar & TabButton Example**
```qml
TabBar {
id: tabBar
currentIndex: 0
TabButton { text: "foo" } // will change currentIndex to 0 if selected
TabButton { text: "bar" } // will change currentIndex to 1 if selected
...
}
```
`tabBar.currentIndex` can then be used as value for some other property (typically used to supply the index for `StackLayout`)

View File

@ -0,0 +1,266 @@
## 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

96
docs/structure.md Normal file
View File

@ -0,0 +1,96 @@
## Structure
**stack**
* 1. status-go (`src/status/libstatus`)
* 2. nim-status / business logic & persistence (`src/status`)
* 3. initializer wrapper (`src/app/<module>/core.nim`)
* currently contains signals which should be moved into layer 2.
* 4. views & view logic (`src/app/<module>/view.nim` & `ui/*.qml`)
**folder structure**
`src/` - where most of the source is
`src/app` - Where the Application is
`src/app/<module>` - module e.g 'chat', 'profile'
`src/app/<module>/core.nim` - wrapper for this module
`src/app/<module>/view.nim` - view, exposed data and some view specific logic
`src/app/<module>/views/*` - views
`src/signals` - signals (should be refactored & moved into src/status)
`src/status` - business logic
`src/status/libstatus` - integration with status-go
`nim_status_client.nim` - the main file
`ui/` - QML files
**`src/status`**
This folder contains the library that abstracts the status app business logic, it's how the app can interact with status-go as well as how they can obtain data, do actions etc..
* this folder can only import / call files from `src/status/libstatus` (exception for libraries ofc)
* only files in `app/` should be able to import from `src/status` (but never `src/status/libstatus`)
**`src/status/libstatus`**
This folder abstracts the interactions with status-go
* generally this folder should only contain code related to interacting with status-go
* it should not import code from anywhere else (including `src/status`)
* nothing should call libstatus directly
* only the code in `status/` should be able to import / call files in `src/status/libstatus`
**`src/app`**
This folder contains the code related to each section of the app, generally it should be kept to a minimum amount of logic, *it knows what to do, but not how to do it*
**`src/app/<module>/`**
* each `<module>` folder inside `app/` should correspond to a section in the app (exception for the `onboarding/` and `login/` currently)
* there should be no new folders here unless we are adding a brand new section to the sidebar
* files inside a `<module>` should not import files from another `<module>`
* while the code here can react to events emited by nim-status (`src/status`) it should not be able to emit events
**`src/app/<module>/core.nim`**
This file is the controller of this module, the general structure of controller is typically predictable and always the same
* it imports a view
* it imports the nim-status lib
* it contains an `init` method
* it exposes a QVariant
the constructor has typically the following structure
```nimrod=
type NodeController* = ref object of SignalSubscriber
status*: Status
view*: NodeView
variant*: QVariant
proc newController*(status: Status): NodeController =
result = NodeController()
result.status = status
result.view = newNodeView(status)
result.variant = newQVariant(result.view)
method onSignal(self: NodeController, data: Signal) =
var msg = cast[WalletSignal](data)
# Do something with the signal...
```
* with the exception of `src/status/` and its own files within `src/app/<module>` (i.e the views), a controller should **not** import files from anywhere else (including other files inside `app/`)
**`src/app/<module>/view.nim`**
This file contains the main QtObject for this `<module>` and exposes methods to interact with the views for the controller and QML.
* this file cannot import any other file except:
* other views within this `<module>`
* `src/status/` to use their types
* if there are multiple subviews, then they should go into the `views/` folder and initialized in this file.
## Future directions
* signals will be refactored/moved from core.nim files and `signals/` into `src/status/` and instead handle as events
* instead of importing `src/status/libstatus` in `src/status` files, we will do dependency injection, this allow us to more easily do unit tests, as well as transition from status-go to nimstatus
* `src/status` should be reanamed to `src/nim-status`
* `src/status/libstatus` should be renamed to `src/nim-status/status-go`

106
docs/tips.md Normal file
View File

@ -0,0 +1,106 @@
## tips and tricks
### seeing output of macros
```nimrod=
import macros
expandmacros:
#code
```
then during compilation it will display what the expanded code looks like
### Getting notified for QML properties changing
Each QML property has an `onChange` attached to it automatically.
For example, if you a property named `name`, it will have an `onNameChanged`. It follows the pattern: `on` + Property + `Change`.
Eg:
```
property int index: 0
onIndexChanged: {
console.log('Index changed', index)
}
```
## Async
```nimrod
import chronos
proc someFunction*(someParameter:int): Future[string] {.async.}=
result = "Something"
var myResult = waitFor someFunction(1)
# If inside some async function
var myResult = await someFunction(6464435)
# to discard the result,
asyncCheck someFunction(2332)
```
`nim-chronos` API is compatible with https://nim-lang.org/docs/asyncdispatch.html so this page can be used to complement nim-chronos lack of documentation. ([Wiki](https://github.com/status-im/nim-chronos/wiki/AsyncDispatch-comparison))
## Updating data on a QAbstractListModel
While adding/removing values from a list is easy, updating the values of a list requires some extra manipulation:
```
proc updateRecord(idx: int, newValue: string) =
self.myList[idx] = newValue;
var topLeft = self.createIndex(idx,0,nil)
var bottomRight = self.createIndex(idx,0,nil)
self.dataChanged(topLeft, bottomRight, @[RoleNames.SomeRole.int, RoleNames.AnotherRole.int, RoleNames.SomeOtherRole.int])
```
If more than one record is being updated at once, change the `topLeft`'s and `bottomRight`'s `self.createIndex` first parameter to indicate the initial row number and the final row number that were affected.
To refresh the whole table, I think you can use `0` as the first parameter for `createIndex` for both `topLeft` and `bottomRight`.
The final attribute of `dataChanged` is a non-empty sequence of RoleNames containing the attributes that were updated
## Error Handling and Custom Errors
### Raising Custom errors
```nim
type
CustomError* = object of Exception # CatchableError/Defect
try:
raise newException(CustomError, "Some error message")
except CustomError as e:
echo e.msg
```
### Raising Custom Errors with custom data (and parent Error)
```nim
type
CustomError* = object of Exception
customField*: string
type CustomErrorRef = ref CustomError
try:
raise CustomErrorRef(msg: "Some error message", customField: "Some custom error data", parent: (ref ValueError)(msg: "foo bar"))
except CustomError as e:
echo e.msg & ": " & e.customField
echo "Original: " & e.parent.msg
```
### Implementing custom Error helpers with default values
```nim
type
CustomError* = object of Exception
customField*: string
type CustomErrorRef = ref CustomError
proc newCustomError*(customData: string): CustomErrorRef =
result = CustomErrorRef(msg: "This is some custom error message", customField: customData, parent: (ref ValueError)(msg: "Value error"))
```

View File

@ -0,0 +1,73 @@
## Adding a sidebar section
The sidebar and each section is defined at `AppMain.qml`, it contains
* sidebar - `TabBar` with `TabButton` elements
* main section - `StackLayout`
The currently displayed section in the `StackLayout` is determined by the `currentIndex` property, for example `0` will show the first child (in this case `ChatLayout`), `1` will show the second child, and so on
This property is being defined by whatever is the currently selected button in the `Tabbar` with `currentIndex: tabBar.currentIndex`
```qml
TabBar {
id: tabBar
TabButton { ... }
TabButton { ... }
...
}
StackLayout {
...
currentIndex: tabBar.currentIndex
ChatLayout {}
WalletLayout {}
...
}
```
To add a new section, then add a new TabButton to the TabBar, for example:
```qml
TabBar {
...
TabButton {
id: myButton
visible: this.enabled
width: 40
height: this.enabled ? 40 : 0
text: ""
anchors.topMargin: this.enabled ? 50 : 0
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: nodeBtn.top // needs to be previous button
background: Rectangle {
color: Theme.lightBlue
opacity: parent.checked ? 1 : 0
radius: 50
}
Image {
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
fillMode: Image.PreserveAspectFit
source: parent.checked ? "img/node.svg" : "img/node.svg"
}
}
}
```
Then a section to the StackLayout
```qml
StackLayout {
...
Text {
text: "hello world!"
}
}
```
The section can be any qml element, to create your own custom element, see the next section

View File

@ -0,0 +1,72 @@
## Creating a Custom QML component
Creating a custom element typically involves
* creating a new QML file
* adding that QML file in a qmldir file
* adding that QML file to the project `nim-status-client.pro` file (automatic if done in QT Creator)
The easiest way is to do it in QT creator although this can be done manually as well, if not using QT Creator make sure the files are added in the nim-status-client.pro.pro file.
**step 1 - create folder**
In QT creator, go to `app/AppLayouts` right click, and select "New folder", name the folder `MySection`
**step 2 - create QML file**
In `MySection`, right click, and select "Add New", select "QT" -> "QML File (Qt Quick 2)", as a name put `MyQMLComponent.qml` and create the file.
Add the desired content, for example
```qml
import QtQuick 2.0
Item {
Text {
text: "hello"
}
}
```
if not using QT Creator, make sure the files are added in the nim-status-client.pro.pro file, for e.g:
```
DISTFILES += \
app/AppLayouts/MySection/MyQMLComponent.qml \
```
**step 3 - add the component to qmldir**
In `app/AppLayouts/` edit `qmldir` and add the file
```
BrowserLayout 1.0 Browser/BrowserLayout.qml
ChatLayout 1.0 Chat/ChatLayout.qml
NodeLayout 1.0 Node/NodeLayout.qml
ProfileLayout 1.0 Profile/ProfileLayout.qml
WalletLayout 1.0 Wallet/WalletLayout.qml
MyQMLComponent 1.0 MySection/MyQMLComponent.qml
```
This ensures that when `app/AppLayouts/` is imported, the component `MyQMLComponent` will point to the component at `MySection/MyQMLComponent.qml`
**step 4 - use the component**
Note that `AppMain.qml` already imports AppLayouts
```qml
import "./AppLayouts"
```
which makes the `MyQMLComponent` available
In the section created in the `Adding a sidebar section`, replace it with this component
```qml
StackLayout {
...
MyQMLComponent {
}
}
```