diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000000..30119bd121 --- /dev/null +++ b/docs/README.md @@ -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) diff --git a/docs/common_errors.md b/docs/common_errors.md new file mode 100644 index 0000000000..b128a1af2d --- /dev/null +++ b/docs/common_errors.md @@ -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. diff --git a/docs/qml_api.md b/docs/qml_api.md new file mode 100644 index 0000000000..126d94f3f6 --- /dev/null +++ b/docs/qml_api.md @@ -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 + diff --git a/docs/qml_crash_course.md b/docs/qml_crash_course.md new file mode 100644 index 0000000000..ac6180265c --- /dev/null +++ b/docs/qml_crash_course.md @@ -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 + +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`) diff --git a/docs/qml_nim_communication.md b/docs/qml_nim_communication.md new file mode 100644 index 0000000000..dc704a765e --- /dev/null +++ b/docs/qml_nim_communication.md @@ -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//view.nim` and `src/app//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//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 diff --git a/docs/structure.md b/docs/structure.md new file mode 100644 index 0000000000..1418f9836f --- /dev/null +++ b/docs/structure.md @@ -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//core.nim`) + * currently contains signals which should be moved into layer 2. +* 4. views & view logic (`src/app//view.nim` & `ui/*.qml`) + +**folder structure** + +`src/` - where most of the source is +`src/app` - Where the Application is +`src/app/` - module e.g 'chat', 'profile' +`src/app//core.nim` - wrapper for this module +`src/app//view.nim` - view, exposed data and some view specific logic +`src/app//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//`** + +* each `` 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 `` should not import files from another `` +* while the code here can react to events emited by nim-status (`src/status`) it should not be able to emit events + +**`src/app//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/` (i.e the views), a controller should **not** import files from anywhere else (including other files inside `app/`) + +**`src/app//view.nim`** + +This file contains the main QtObject for this `` 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 `` + * `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` \ No newline at end of file diff --git a/docs/tips.md b/docs/tips.md new file mode 100644 index 0000000000..672121b4c6 --- /dev/null +++ b/docs/tips.md @@ -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")) +``` \ No newline at end of file diff --git a/docs/tutorial_adding_section.md b/docs/tutorial_adding_section.md new file mode 100644 index 0000000000..e004e55312 --- /dev/null +++ b/docs/tutorial_adding_section.md @@ -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 diff --git a/docs/tutorial_custom_component.md b/docs/tutorial_custom_component.md new file mode 100644 index 0000000000..8b627ed9c4 --- /dev/null +++ b/docs/tutorial_custom_component.md @@ -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 { + } +} +```