diff --git a/.gitignore b/.gitignore index 6e52fa4522039a6336bc278f9d2b6217f194a35e..7ff4882a41ca87f23f0d46663f615bc66a0adaba 100644 --- a/.gitignore +++ b/.gitignore @@ -46,3 +46,4 @@ res/i18n/lang-* out .directory temp +*_uic.py \ No newline at end of file diff --git a/gen_resources.py b/gen_resources.py index 2a269dfe5b9d6c49d537caea6dd20b9eb3d92d4b..cfa45fd9918bf98dc408f1416efbc18a742419a0 100644 --- a/gen_resources.py +++ b/gen_resources.py @@ -2,8 +2,10 @@ # -*- coding: utf-8 -*- import sys, os, multiprocessing, subprocess + +sakia = os.path.abspath(os.path.join(os.path.dirname(__file__))) resources = os.path.abspath(os.path.join(os.path.dirname(__file__), 'res')) -gen_ui = os.path.abspath(os.path.join(os.path.dirname(__file__), 'src', 'sakia', 'gen_resources')) +gen_ui = os.path.abspath(os.path.join(os.path.dirname(__file__), 'src', 'sakia', 'presentation')) gen_resources = os.path.abspath(os.path.join(os.path.dirname(__file__), 'src')) def convert_ui(args, **kwargs): @@ -12,11 +14,15 @@ def convert_ui(args, **kwargs): def build_resources(): try: to_process = [] - for root, dirs, files in os.walk(resources): + for root, dirs, files in os.walk(sakia): for f in files: if f.endswith('.ui'): source = os.path.join(root, f) - dest = os.path.join(gen_ui, os.path.splitext(os.path.basename(source))[0]+'_uic.py') + if os.path.commonpath([resources, root]) == resources: + dest = os.path.join(gen_ui, os.path.splitext(os.path.basename(source))[0]+'_uic.py') + else: + dest = os.path.join(root, os.path.splitext(os.path.basename(source))[0]+'_uic.py') + exe = 'pyuic5' elif f.endswith('.qrc'): source = os.path.join(root, f) diff --git a/res/ui/community_view.ui b/res/ui/community_view.ui index 213060dc7294aa314a1b2b10781c79ac7865d500..8827fe266a1f83882dcac7b74cab8c299502e0db 100644 --- a/res/ui/community_view.ui +++ b/res/ui/community_view.ui @@ -14,125 +14,6 @@ <string>Form</string> </property> <layout class="QVBoxLayout" name="verticalLayout"> - <item> - <widget class="QFrame" name="frame"> - <property name="frameShape"> - <enum>QFrame::StyledPanel</enum> - </property> - <property name="frameShadow"> - <enum>QFrame::Raised</enum> - </property> - <layout class="QHBoxLayout" name="horizontalLayout"> - <item> - <widget class="QPushButton" name="button_home"> - <property name="sizePolicy"> - <sizepolicy hsizetype="Fixed" vsizetype="Fixed"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> - </property> - <property name="text"> - <string/> - </property> - <property name="icon"> - <iconset resource="../icons/icons.qrc"> - <normaloff>:/icons/home_icon</normaloff>:/icons/home_icon</iconset> - </property> - <property name="iconSize"> - <size> - <width>32</width> - <height>32</height> - </size> - </property> - </widget> - </item> - <item> - <widget class="QLabel" name="label_currency"> - <property name="text"> - <string/> - </property> - </widget> - </item> - <item> - <widget class="QPushButton" name="button_send_money"> - <property name="text"> - <string>Send money</string> - </property> - <property name="icon"> - <iconset resource="../icons/icons.qrc"> - <normaloff>:/icons/payment_icon</normaloff>:/icons/payment_icon</iconset> - </property> - <property name="iconSize"> - <size> - <width>32</width> - <height>32</height> - </size> - </property> - </widget> - </item> - <item> - <widget class="QPushButton" name="button_certification"> - <property name="text"> - <string>Certification</string> - </property> - <property name="icon"> - <iconset resource="../icons/icons.qrc"> - <normaloff>:/icons/certification_icon</normaloff>:/icons/certification_icon</iconset> - </property> - <property name="iconSize"> - <size> - <width>32</width> - <height>32</height> - </size> - </property> - </widget> - </item> - <item> - <widget class="QPushButton" name="button_membership"> - <property name="text"> - <string>Renew membership</string> - </property> - <property name="icon"> - <iconset resource="../icons/icons.qrc"> - <normaloff>:/icons/renew_membership</normaloff>:/icons/renew_membership</iconset> - </property> - <property name="iconSize"> - <size> - <width>32</width> - <height>32</height> - </size> - </property> - </widget> - </item> - <item> - <widget class="QToolButton" name="toolbutton_menu"> - <property name="text"> - <string/> - </property> - <property name="icon"> - <iconset resource="../icons/icons.qrc"> - <normaloff>:/icons/menu_icon</normaloff>:/icons/menu_icon</iconset> - </property> - <property name="iconSize"> - <size> - <width>32</width> - <height>32</height> - </size> - </property> - <property name="popupMode"> - <enum>QToolButton::InstantPopup</enum> - </property> - <property name="autoRaise"> - <bool>false</bool> - </property> - <property name="arrowType"> - <enum>Qt::NoArrow</enum> - </property> - </widget> - </item> - </layout> - </widget> - </item> <item> <widget class="QTabWidget" name="tabs"/> </item> diff --git a/src/sakia/gui/agent/__init__.py b/src/sakia/gui/agent/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..03eacd239fc9b25aa6e2df24779250abd78e79cd --- /dev/null +++ b/src/sakia/gui/agent/__init__.py @@ -0,0 +1 @@ +__all__ = ['controller', 'model'] diff --git a/src/sakia/gui/agent/controller.py b/src/sakia/gui/agent/controller.py new file mode 100644 index 0000000000000000000000000000000000000000..645bda03bbb7e18cb686665deb599b56804ceb25 --- /dev/null +++ b/src/sakia/gui/agent/controller.py @@ -0,0 +1,22 @@ +from PyQt5.QtCore import QObject + + +class AgentController(QObject): + """ + The navigation panel + """ + + def __init__(self, parent, view, model): + """ + Constructor of the navigation agent + + :param PyQt5.QtWidgets.QWidget presentation: the presentation + :param sakia.core.gui.navigation.model.NavigationModel model: the model + """ + super().__init__(parent) + self.view = view + self.model = model + + def attach(self, controller): + controller.setParent(self) + return controller diff --git a/src/sakia/gui/agent/model.py b/src/sakia/gui/agent/model.py new file mode 100644 index 0000000000000000000000000000000000000000..61a2d366bd93c60c1ed4ec9d386983fcdf822511 --- /dev/null +++ b/src/sakia/gui/agent/model.py @@ -0,0 +1,19 @@ +from PyQt5.QtCore import QObject + + +class AgentModel(QObject): + """ + An agent + """ + + def __init__(self, parent): + """ + Constructor of an agent + + :param sakia.core.gui.agent.controller.AbstractAgentController controller: the controller + """ + super().__init__(parent) + + @property + def account(self): + return self.app.current_account diff --git a/src/sakia/gui/certification.py b/src/sakia/gui/certification.py index f19a2840f8edd3c566ff446b065525db639b87ae..b50d9d5e110d141b6ca7eb2931f0e17d5a29cad4 100644 --- a/src/sakia/gui/certification.py +++ b/src/sakia/gui/certification.py @@ -14,7 +14,8 @@ from .widgets.dialogs import QAsyncMessageBox from .member import MemberDialog from ..tools.decorators import asyncify, once_at_a_time from ..tools.exceptions import NoPeerAvailable -from ..gen_resources.certification_uic import Ui_CertificationDialog +from ..presentation.certification_uic import Ui_CertificationDialog + class CertificationDialog(QObject): """ diff --git a/src/sakia/gui/community_view.py b/src/sakia/gui/community_view.py index 53f47851bb33874355621ab45729821b67fc6e20..1255e5570900db94a87a1c5a6ad321e8e25bbd90 100644 --- a/src/sakia/gui/community_view.py +++ b/src/sakia/gui/community_view.py @@ -13,13 +13,12 @@ from PyQt5.QtWidgets import QWidget, QMessageBox, QDialog, QPushButton, QTabBar, from .graphs.wot_tab import WotTabWidget from .widgets import toast -from .widgets.dialogs import QAsyncMessageBox, QAsyncFileDialog, dialog_async_exec from .identities_tab import IdentitiesTabWidget from .informations_tab import InformationsTabWidget from .network_tab import NetworkTabWidget from .transactions_tab import TransactionsTabWidget from .graphs.explorer_tab import ExplorerTabWidget -from ..gen_resources.community_view_uic import Ui_CommunityWidget +from ..presentation.community_view_uic import Ui_CommunityWidget from ..tools.decorators import asyncify, once_at_a_time, cancel_once_task from ..tools.exceptions import MembershipNotFoundError, LookupFailureError, NoPeerAvailable @@ -35,10 +34,6 @@ class CommunityWidget(QWidget, Ui_CommunityWidget): _tab_identities_label = QT_TRANSLATE_NOOP("CommunityWidget", "Search Identities") _tab_network_label = QT_TRANSLATE_NOOP("CommunityWidget", "Network") _tab_informations_label = QT_TRANSLATE_NOOP("CommunityWidget", "Informations") - _action_showinfo_text = QT_TRANSLATE_NOOP("CommunityWidget", "Show informations") - _action_explore_text = QT_TRANSLATE_NOOP("CommunityWidget", "Explore the Web of Trust") - _action_publish_uid_text = QT_TRANSLATE_NOOP("CommunityWidget", "Publish UID") - _action_revoke_uid_text = QT_TRANSLATE_NOOP("CommunityWidget", "Revoke UID") def __init__(self, app, status_label, label_icon): """ @@ -61,16 +56,8 @@ class CommunityWidget(QWidget, Ui_CommunityWidget): self.tab_network = NetworkTabWidget(self.app) self.tab_explorer = ExplorerTabWidget(self.app) - self.action_publish_uid = QAction(self.tr(CommunityWidget._action_publish_uid_text), self) - self.action_revoke_uid = QAction(self.tr(CommunityWidget._action_revoke_uid_text), self) - self.action_showinfo = QAction(self.tr(CommunityWidget._action_showinfo_text), self) - self.action_explorer = QAction(self.tr(CommunityWidget._action_explore_text), self) - super().setupUi(self) - tool_menu = QMenu(self.tr("Tools"), self.toolbutton_menu) - self.toolbutton_menu.setMenu(tool_menu) - self.tab_identities.view_in_wot.connect(self.tab_wot.draw_graph) self.tab_identities.view_in_wot.connect(lambda: self.tabs.setCurrentWidget(self.tab_wot.widget)) self.tab_history.view_in_wot.connect(self.tab_wot.draw_graph) @@ -94,27 +81,6 @@ class CommunityWidget(QWidget, Ui_CommunityWidget): QIcon(":/icons/network_icon"), self.tr("Network")) - action_showinfo = QAction(self.tr("Show informations"), self.toolbutton_menu) - action_showinfo.triggered.connect(lambda : self.show_closable_tab(self.tab_informations, - QIcon(":/icons/informations_icon"), self.tr("Informations"))) - tool_menu.addAction(action_showinfo) - - action_showexplorer = QAction(self.tr("Show explorer"), self.toolbutton_menu) - action_showexplorer.triggered.connect(lambda : self.show_closable_tab(self.tab_explorer.widget, - QIcon(":/icons/explorer_icon"), self.tr("Explorer"))) - tool_menu.addAction(action_showexplorer) - - menu_advanced = QMenu(self.tr("Advanced"), self.toolbutton_menu) - action_gen_revokation = QAction(self.tr("Save revokation document"), menu_advanced) - action_gen_revokation.triggered.connect(self.action_save_revokation) - menu_advanced.addAction(action_gen_revokation) - tool_menu.addMenu(menu_advanced) - - self.action_publish_uid.triggered.connect(self.publish_uid) - tool_menu.addAction(self.action_publish_uid) - - self.button_membership.clicked.connect(self.send_membership_demand) - def show_closable_tab(self, tab, icon, title): if self.tabs.indexOf(tab) == -1: self.tabs.addTab(tab, icon, title) @@ -175,31 +141,6 @@ class CommunityWidget(QWidget, Ui_CommunityWidget): error, QMessageBox.Ok) - @asyncify - async def action_save_revokation(self, checked=False): - password = await self.password_asker.async_exec() - if self.password_asker.result() == QDialog.Rejected: - return - - raw_document = await self.account.generate_revokation(self.community, password) - # Testable way of using a QFileDialog - selected_files = await QAsyncFileDialog.get_save_filename(self, self.tr("Save a revokation document"), - "", self.tr("All text files (*.txt)")) - if selected_files: - path = selected_files[0] - if not path.endswith('.txt'): - path = "{0}.txt".format(path) - with open(path, 'w') as save_file: - save_file.write(raw_document) - - dialog = QMessageBox(QMessageBox.Information, self.tr("Revokation file"), - self.tr("""<div>Your revokation document has been saved.</div> -<div><b>Please keep it in a safe place.</b></div> -The publication of this document will remove your identity from the network.</p>"""), QMessageBox.Ok, - self) - dialog.setTextFormat(Qt.RichText) - await dialog_async_exec(dialog) - @once_at_a_time @asyncify async def refresh_block(self, block_number): @@ -317,40 +258,6 @@ The publication of this document will remove your identity from the network.</p> self.status_label.setText(label_text) self.label_icon.setPixmap(QPixmap(icon).scaled(24, 24, Qt.KeepAspectRatio, Qt.SmoothTransformation)) - @once_at_a_time - @asyncify - async def refresh_quality_buttons(self): - if self.account and self.community: - try: - account_identity = await self.account.identity(self.community) - published_uid = await account_identity.published_uid(self.community) - uid_is_revokable = await account_identity.uid_is_revokable(self.community) - if published_uid: - logging.debug("UID Published") - self.action_revoke_uid.setEnabled(uid_is_revokable) - is_member = await account_identity.is_member(self.community) - if is_member: - self.button_membership.setText(self.tr("Renew membership")) - self.button_membership.setEnabled(True) - self.button_certification.setEnabled(True) - self.action_publish_uid.setEnabled(False) - else: - logging.debug("Not a member") - self.button_membership.setText(self.tr("Send membership demand")) - self.button_membership.setEnabled(True) - self.action_publish_uid.setEnabled(False) - if await self.community.get_block(0) is not None: - self.button_certification.setEnabled(False) - else: - logging.debug("UID not published") - self.button_membership.setEnabled(False) - self.button_certification.setEnabled(False) - self.action_publish_uid.setEnabled(True) - except LookupFailureError: - self.button_membership.setEnabled(False) - self.button_certification.setEnabled(False) - self.action_publish_uid.setEnabled(False) - def showEvent(self, event): self.refresh_status() @@ -360,69 +267,6 @@ The publication of this document will remove your identity from the network.</p> self.tab_history.refresh_balance() self.tab_informations.refresh() - @asyncify - async def send_membership_demand(self, checked=False): - password = await self.password_asker.async_exec() - if self.password_asker.result() == QDialog.Rejected: - return - result = await self.account.send_membership(password, self.community, 'IN') - if result[0]: - if self.app.preferences['notifications']: - toast.display(self.tr("Membership"), self.tr("Success sending Membership demand")) - else: - await QAsyncMessageBox.information(self, self.tr("Membership"), - self.tr("Success sending Membership demand")) - else: - if self.app.preferences['notifications']: - toast.display(self.tr("Membership"), result[1]) - else: - await QAsyncMessageBox.critical(self, self.tr("Membership"), - result[1]) - - @asyncify - async def send_membership_leaving(self): - reply = await QAsyncMessageBox.warning(self, self.tr("Warning"), - self.tr("""Are you sure ? -Sending a leaving demand cannot be canceled. -The process to join back the community later will have to be done again.""") -.format(self.account.pubkey), QMessageBox.Ok | QMessageBox.Cancel) - if reply == QMessageBox.Ok: - password = self.password_asker.exec_() - if self.password_asker.result() == QDialog.Rejected: - return - result = await self.account.send_membership(password, self.community, 'OUT') - if result[0]: - if self.app.preferences['notifications']: - toast.display(self.tr("Revoke"), self.tr("Success sending Revoke demand")) - else: - await QAsyncMessageBox.information(self, self.tr("Revoke"), - self.tr("Success sending Revoke demand")) - else: - if self.app.preferences['notifications']: - toast.display(self.tr("Revoke"), result[1]) - else: - await QAsyncMessageBox.critical(self, self.tr("Revoke"), - result[1]) - - @asyncify - async def publish_uid(self, checked=False): - password = await self.password_asker.async_exec() - if self.password_asker.result() == QDialog.Rejected: - return - result = await self.account.send_selfcert(password, self.community) - if result[0]: - if self.app.preferences['notifications']: - toast.display(self.tr("UID"), self.tr("Success publishing your UID")) - else: - await QAsyncMessageBox.information(self, self.tr("Membership"), - self.tr("Success publishing your UID")) - else: - if self.app.preferences['notifications']: - toast.display(self.tr("UID"), result[1]) - else: - await QAsyncMessageBox.critical(self, self.tr("UID"), - result[1]) - def retranslateUi(self, widget): """ Method to complete translations missing from generated code @@ -434,9 +278,6 @@ The process to join back the community later will have to be done again.""") self.tabs.setTabText(self.tabs.indexOf(self.tab_informations), self.tr(CommunityWidget._tab_informations_label)) self.tabs.setTabText(self.tabs.indexOf(self.tab_history.widget), self.tr(CommunityWidget._tab_history_label)) self.tabs.setTabText(self.tabs.indexOf(self.tab_identities.widget), self.tr(CommunityWidget._tab_identities_label)) - self.action_publish_uid.setText(self.tr(CommunityWidget._action_publish_uid_text)) - self.action_revoke_uid.setText(self.tr(CommunityWidget._action_revoke_uid_text)) - self.action_showinfo.setText(self.tr(CommunityWidget._action_showinfo_text)) super().retranslateUi(self) def showEvent(self, QShowEvent): diff --git a/src/sakia/gui/contact.py b/src/sakia/gui/contact.py index 8d3036b91222e6f993f91173a9294fdad1bc6a0b..0cac15ad5b289543e4e0265b1cf23d7eca2af600 100644 --- a/src/sakia/gui/contact.py +++ b/src/sakia/gui/contact.py @@ -9,7 +9,7 @@ import logging from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QMessageBox from ..core.registry import IdentitiesRegistry from ..tools.exceptions import ContactAlreadyExists -from ..gen_resources.contact_uic import Ui_ConfigureContactDialog +from ..presentation.contact_uic import Ui_ConfigureContactDialog class ConfigureContactDialog(QDialog, Ui_ConfigureContactDialog): diff --git a/src/sakia/gui/graphs/explorer_tab.py b/src/sakia/gui/graphs/explorer_tab.py index 78da13791d23c561ea03600e5662c2fcfcb54993..66ac4c7bd9abce3c904305633afd593b130593d5 100644 --- a/src/sakia/gui/graphs/explorer_tab.py +++ b/src/sakia/gui/graphs/explorer_tab.py @@ -7,7 +7,7 @@ from ...tools.exceptions import NoPeerAvailable from ...tools.decorators import asyncify, once_at_a_time, cancel_once_task from ...core.graph import ExplorerGraph from .graph_tab import GraphTabWidget -from ...gen_resources.explorer_tab_uic import Ui_ExplorerTabWidget +from ...presentation.explorer_tab_uic import Ui_ExplorerTabWidget class ExplorerTabWidget(GraphTabWidget, Ui_ExplorerTabWidget): diff --git a/src/sakia/gui/graphs/wot_tab.py b/src/sakia/gui/graphs/wot_tab.py index 31eedfdce5b7470cc8343bc5ee1dce59ec012c56..6ee4ff1213a3a040ba7dc822ff5dc3eecd8b00a5 100644 --- a/src/sakia/gui/graphs/wot_tab.py +++ b/src/sakia/gui/graphs/wot_tab.py @@ -5,7 +5,7 @@ from PyQt5.QtCore import QEvent, pyqtSignal, QT_TRANSLATE_NOOP, QObject from PyQt5.QtWidgets import QWidget from ...tools.decorators import asyncify, once_at_a_time, cancel_once_task from ...core.graph import WoTGraph -from ...gen_resources.wot_tab_uic import Ui_WotTabWidget +from ...presentation.wot_tab_uic import Ui_WotTabWidget from ...gui.widgets.busy import Busy from .graph_tab import GraphTabWidget diff --git a/src/sakia/gui/homescreen.py b/src/sakia/gui/homescreen.py index 3eb44eadf63bb86595608e6baabae20bf93c3714..0f0618b11f6ff38018cccc0dbd64ed634ddc364b 100644 --- a/src/sakia/gui/homescreen.py +++ b/src/sakia/gui/homescreen.py @@ -6,7 +6,7 @@ Created on 31 janv. 2015 from PyQt5.QtWidgets import QWidget, QFrame, QGridLayout, QAction from PyQt5.QtCore import QEvent, Qt, pyqtSlot, pyqtSignal -from ..gen_resources.homescreen_uic import Ui_HomescreenWidget +from ..presentation.homescreen_uic import Ui_HomescreenWidget from .community_tile import CommunityTile from ..core.community import Community import logging diff --git a/src/sakia/gui/identities_tab.py b/src/sakia/gui/identities_tab.py index 21e5ab64e61f2f5ee56208bfdee97bfcfc56c90d..10b0f0cc4afee5cb276f3d9532c84b52ef83451e 100644 --- a/src/sakia/gui/identities_tab.py +++ b/src/sakia/gui/identities_tab.py @@ -14,7 +14,7 @@ from duniterpy.api import bma, errors from duniterpy.documents import BlockUID from ..models.identities import IdentitiesFilterProxyModel, IdentitiesTableModel -from ..gen_resources.identities_tab_uic import Ui_IdentitiesTab +from ..presentation.identities_tab_uic import Ui_IdentitiesTab from ..core.registry import Identity, BlockchainState from ..tools.exceptions import NoPeerAvailable from ..tools.decorators import asyncify, once_at_a_time, cancel_once_task diff --git a/src/sakia/gui/import_account.py b/src/sakia/gui/import_account.py index 59d73ff48fd9b2839406094b297ecd2b5c0ed25d..3c3d6e361fec88bfbe4d3fb31d84bce4fa47a004 100644 --- a/src/sakia/gui/import_account.py +++ b/src/sakia/gui/import_account.py @@ -7,7 +7,7 @@ import re from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QMessageBox, QFileDialog from ..tools.exceptions import Error -from ..gen_resources.import_account_uic import Ui_ImportAccountDialog +from ..presentation.import_account_uic import Ui_ImportAccountDialog class ImportAccountDialog(QDialog, Ui_ImportAccountDialog): diff --git a/src/sakia/gui/informations_tab.py b/src/sakia/gui/informations_tab.py index af6be48e89a9534237fa120560e25c2ea6355819..c46342aa596fb70478435d7a76c9511626bc80eb 100644 --- a/src/sakia/gui/informations_tab.py +++ b/src/sakia/gui/informations_tab.py @@ -8,7 +8,7 @@ import logging import math from PyQt5.QtCore import QLocale, QDateTime, QEvent from PyQt5.QtWidgets import QWidget -from ..gen_resources.informations_tab_uic import Ui_InformationsTabWidget +from ..presentation.informations_tab_uic import Ui_InformationsTabWidget from ..tools.decorators import asyncify, once_at_a_time, cancel_once_task from ..tools.exceptions import NoPeerAvailable from .widgets import Busy diff --git a/src/sakia/gen_resources/__init__.py b/src/sakia/gui/main_window/__init__.py similarity index 100% rename from src/sakia/gen_resources/__init__.py rename to src/sakia/gui/main_window/__init__.py diff --git a/src/sakia/gui/main_window/controller.py b/src/sakia/gui/main_window/controller.py new file mode 100644 index 0000000000000000000000000000000000000000..b80d1713c0727aab4f09c6c070468151e20494ae --- /dev/null +++ b/src/sakia/gui/main_window/controller.py @@ -0,0 +1,119 @@ +""" +Created on 1 févr. 2014 + +@author: inso +""" +import aiohttp +import logging +import traceback + +from PyQt5.QtWidgets import QMessageBox, QApplication +from PyQt5.QtCore import QEvent, pyqtSlot, QObject +from PyQt5.QtGui import QIcon + +from ..password_asker import PasswordAskerDialog +from ...__init__ import __version__ +from ..widgets import toast +from ..agent.controller import AgentController +from .view import MainWindowView +from .model import MainWindowModel +from ..status_bar.controller import StatusBarController + + +class MainWindowController(AgentController): + """ + classdocs + """ + + def __init__(self, view, model, password_asker, status_bar): + """ + Init + :param MainWindowView view: the ui of the mainwindow agent + :param sakia.gui.main_window.model.MainWindowModel: the model of the mainwindow agent + :param sakia.gui.status_bar.controller.StatusBarController: the controller of the status bar agent + + :param PasswordAsker password_asker: the password asker of the application + :type: sakia.core.app.Application + """ + # Set up the user interface from Designer. + super().__init__(None, view, model) + self.initialized = False + self.password_asker = password_asker + if status_bar: + self.status_bar = self.attach(status_bar) + + QApplication.setWindowIcon(QIcon(":/icons/sakia_logo")) + + @classmethod + def startup(cls, app): + view = MainWindowView(None) + model = MainWindowModel(None, app) + main_window = cls(view, model,PasswordAskerDialog(None), None) + main_window.status_bar = main_window.attach(StatusBarController.create(main_window, app)) + main_window.view.setStatusBar(main_window.status_bar.view) + #app.version_requested.connect(main_window.latest_version_requested) + #app.account_imported.connect(main_window.import_account_accepted) + #app.account_changed.connect(main_window.change_account) + + view.showMaximized() + main_window.refresh() + return main_window + + def change_account(self): + if self.account: + self.account.contacts_changed.disconnect(self.refresh_contacts) + self.account = self.app.current_account + self.password_asker.change_account(self.account) + self.refresh() + + @pyqtSlot(str) + def display_error(self, error): + QMessageBox.critical(self, ":(", + error, + QMessageBox.Ok) + + @pyqtSlot(int) + def referential_changed(self, index): + if self.account: + self.account.set_display_referential(index) + if self.community_view: + self.community_view.referential_changed() + self.homescreen.referential_changed() + + @pyqtSlot() + def latest_version_requested(self): + latest = self.app.available_version + logging.debug("Latest version requested") + if not latest[0]: + version_info = self.tr("Please get the latest release {version}") \ + .format(version=latest[1]) + version_url = latest[2] + + if self.app.preferences['notifications']: + toast.display("sakia", """{version_info}""".format( + version_info=version_info, + version_url=version_url)) + + def refresh(self): + """ + Refresh main window + When the selected account changes, all the widgets + in the window have to be refreshed + """ + self.status_bar.refresh() + self.view.setWindowTitle(self.tr("sakia {0}").format(__version__)) + + def eventFilter(self, target, event): + """ + Event filter on the widget + :param QObject target: the target of the event + :param QEvent event: the event + :return: bool + """ + if target == self.widget: + if event.type() == QEvent.LanguageChange: + self.ui.retranslateUi(self) + self.refresh() + return self.widget.eventFilter(target, event) + return False + diff --git a/res/ui/mainwindow.ui b/src/sakia/gui/main_window/mainwindow.ui similarity index 63% rename from res/ui/mainwindow.ui rename to src/sakia/gui/main_window/mainwindow.ui index d92357d930a1c8257d97ae0eaa38dbe2dfe9b4f5..b689d2b854e5d7d6aa43169d0b80ea013feedc32 100644 --- a/res/ui/mainwindow.ui +++ b/src/sakia/gui/main_window/mainwindow.ui @@ -14,76 +14,11 @@ <string notr="true">Sakia</string> </property> <widget class="QWidget" name="centralwidget"> - <layout class="QVBoxLayout" name="verticalLayout_6"/> - </widget> - <widget class="QMenuBar" name="menubar"> - <property name="geometry"> - <rect> - <x>0</x> - <y>0</y> - <width>900</width> - <height>30</height> - </rect> - </property> - <widget class="QMenu" name="menu_file"> - <property name="title"> - <string>Fi&le</string> - </property> - <addaction name="action_import"/> - <addaction name="action_export"/> - <addaction name="separator"/> - <addaction name="actionPreferences"/> - <addaction name="action_quit"/> - </widget> - <widget class="QMenu" name="menu_account"> - <property name="title"> - <string>Acco&unt</string> - </property> - <widget class="QMenu" name="menu_contacts_list"> - <property name="title"> - <string>Co&ntacts</string> - </property> - <addaction name="separator"/> - </widget> - <widget class="QMenu" name="menu_change_account"> - <property name="title"> - <string>&Open</string> - </property> - </widget> - <widget class="QMenu" name="menuAdvanced"> - <property name="title"> - <string>Advanced</string> - </property> - <addaction name="action_revoke_identity"/> - </widget> - <addaction name="menu_change_account"/> - <addaction name="action_configure_parameters"/> - <addaction name="action_add_account"/> - <addaction name="separator"/> - <addaction name="actionCertification"/> - <addaction name="actionTransfer_money"/> - <addaction name="separator"/> - <addaction name="action_add_a_contact"/> - <addaction name="menu_contacts_list"/> - <addaction name="separator"/> - <addaction name="menuAdvanced"/> - </widget> - <widget class="QMenu" name="menu_help"> - <property name="title"> - <string>&Help</string> - </property> - <addaction name="actionAbout"/> - </widget> - <widget class="QMenu" name="menu_duniter"> - <property name="title"> - <string>&Duniter</string> - </property> - <addaction name="actionManage_local_node"/> - </widget> - <addaction name="menu_file"/> - <addaction name="menu_account"/> - <addaction name="menu_duniter"/> - <addaction name="menu_help"/> + <layout class="QVBoxLayout" name="verticalLayout_6"> + <item> + <layout class="QHBoxLayout" name="horizontalLayout"/> + </item> + </layout> </widget> <widget class="QStatusBar" name="statusbar"/> <action name="actionManage_accounts"> @@ -198,7 +133,7 @@ </action> <action name="action_revoke_identity"> <property name="text"> - <string>Revoke an identity</string> + <string>&Revoke an identity</string> </property> </action> </widget> diff --git a/src/sakia/gui/main_window/model.py b/src/sakia/gui/main_window/model.py new file mode 100644 index 0000000000000000000000000000000000000000..32af1551002eed101f94c0f0999ac086ffc1e217 --- /dev/null +++ b/src/sakia/gui/main_window/model.py @@ -0,0 +1,15 @@ +from sakia.gui.agent.model import AgentModel +from sakia.core.money import Referentials + + +class MainWindowModel(AgentModel): + """ + The model of Navigation agent + """ + + def __init__(self, parent, app): + super().__init__(parent) + self.app = app + + def referentials(self): + return Referentials diff --git a/src/sakia/gui/main_window/view.py b/src/sakia/gui/main_window/view.py new file mode 100644 index 0000000000000000000000000000000000000000..784bc7ddeb257be8d18997ff08728bf027998cea --- /dev/null +++ b/src/sakia/gui/main_window/view.py @@ -0,0 +1,12 @@ +from PyQt5.QtWidgets import QMainWindow +from .mainwindow_uic import Ui_MainWindow + + +class MainWindowView(QMainWindow, Ui_MainWindow): + """ + The model of Navigation agent + """ + + def __init__(self, parent): + super().__init__(parent) + self.setupUi(self) diff --git a/src/sakia/gui/mainwindow.py b/src/sakia/gui/mainwindow.py deleted file mode 100644 index 5d66b78e060f2d8ad39799d10ec09d94e7944b9a..0000000000000000000000000000000000000000 --- a/src/sakia/gui/mainwindow.py +++ /dev/null @@ -1,460 +0,0 @@ -""" -Created on 1 févr. 2014 - -@author: inso -""" -import aiohttp -import logging -import traceback - -from PyQt5.QtWidgets import QMainWindow, QAction, QFileDialog, \ - QMessageBox, QLabel, QComboBox, QDialog, QApplication, QErrorMessage -from PyQt5.QtCore import QLocale, QEvent, \ - pyqtSlot, QDateTime, QTimer, Qt, QObject, QUrl -from PyQt5.QtGui import QIcon - -from ..gen_resources.mainwindow_uic import Ui_MainWindow -from ..gen_resources.about_uic import Ui_AboutPopup -from .process_cfg_account import ProcessConfigureAccount -from .transfer import TransferMoneyDialog -from .community_view import CommunityWidget -from .contact import ConfigureContactDialog -from .import_account import ImportAccountDialog -from .certification import CertificationDialog -from .revocation import RevocationDialog -from .password_asker import PasswordAskerDialog -from .preferences import PreferencesDialog -from .process_cfg_community import ProcessConfigureCommunity -from .homescreen import HomeScreenWidget -from .node_manager import NodeManager -from ..core import money -from ..core.community import Community -from ..tools.decorators import asyncify -from ..__init__ import __version__ -from .widgets import toast -from .widgets.dialogs import QAsyncMessageBox - - -class MainWindow(QObject): - """ - classdocs - """ - - def __init__(self, app, account, homescreen, community_view, node_manager, - widget, ui, - label_icon, label_status, label_time, combo_referential, - password_asker): - """ - Init - :param sakia.core.app.Application app: application - :param sakia.core.Account account: the account - :param HomeScreenWidgetcreen homescreen: the homescreen - :param CommunityView community_view: the community view - :param NodeManager node_manager: the local node manager dialog - :param QMainWindow widget: the widget of the main window - :param Ui_MainWindow ui: the ui of the widget - :param QLabel label_icon: the label of the icon in the statusbar - :param QLabel label_status: the label of the status in the statusbar - :param QLabel label_time: the label of the time in the statusbar - :param QCombobox combo_referential: the combo of the referentials in the statusbar - :param PasswordAsker password_asker: the password asker of the application - :type: sakia.core.app.Application - """ - # Set up the user interface from Designer. - super().__init__() - self.app = app - self.account = account - self.initialized = False - self.password_asker = password_asker - self.import_dialog = None - self.widget = widget - self.ui = ui - self.ui.setupUi(self.widget) - self.widget.installEventFilter(self) - - QApplication.setWindowIcon(QIcon(":/icons/sakia_logo")) - - self.label_icon = label_icon - - self.status_label = label_status - self.status_label.setTextFormat(Qt.RichText) - - self.label_time = label_time - - self.combo_referential = combo_referential - self.combo_referential.setEnabled(False) - self.combo_referential.currentIndexChanged[int].connect(self.referential_changed) - - self.homescreen = homescreen - - self.community_view = community_view - - self.node_manager = node_manager - - - def _init_ui(self): - """ - Connects elements of the UI to the local slots - """ - self.ui.statusbar.addPermanentWidget(self.label_icon, 1) - self.ui.statusbar.addPermanentWidget(self.status_label, 2) - self.ui.statusbar.addPermanentWidget(self.label_time) - self.ui.statusbar.addPermanentWidget(self.combo_referential) - - self.ui.action_add_account.triggered.connect(self.open_add_account_dialog) - self.ui.action_quit.triggered.connect(self.widget.close) - self.ui.actionTransfer_money.triggered.connect(self.open_transfer_money_dialog) - self.ui.action_add_a_contact.triggered.connect(self.open_add_contact_dialog) - self.ui.action_configure_parameters.triggered.connect(self.open_configure_account_dialog) - self.ui.action_import.triggered.connect(self.import_account) - self.ui.action_export.triggered.connect(self.export_account) - self.ui.actionCertification.triggered.connect(self.open_certification_dialog) - self.ui.actionPreferences.triggered.connect(self.open_preferences_dialog) - self.ui.actionAbout.triggered.connect(self.open_about_popup) - self.ui.action_revoke_identity.triggered.connect(self.open_revocation_dialog) - - self.ui.actionManage_local_node.triggered.connect(self.open_duniter_ui) - self.ui.menu_duniter.setDisabled(True) - - def _init_homescreen(self): - """ - Initialize homescreen signals/slots and data - :return: - """ - self.homescreen.status_label = self.status_label - self.homescreen.frame_communities.community_tile_clicked.connect(self.change_community) - self.homescreen.toolbutton_new_account.clicked.connect(self.open_add_account_dialog) - self.homescreen.toolbutton_new_account.addAction(self.ui.action_add_account) - self.homescreen.toolbutton_new_account.addAction(self.ui.action_import) - self.homescreen.button_add_community.clicked.connect(self.action_open_add_community) - self.homescreen.button_disconnect.clicked.connect(lambda :self.action_change_account("")) - self.widget.centralWidget().layout().addWidget(self.homescreen) - self.homescreen.toolbutton_connect.setMenu(self.ui.menu_change_account) - - def _init_community_view(self): - """ - Initialize the community view signals/slots and data - :return: - """ - self.community_view.status_label = self.status_label - self.community_view.label_icon = self.label_icon - self.community_view.button_home.clicked.connect(lambda: self.change_community(None)) - self.community_view.button_certification.clicked.connect(self.open_certification_dialog) - self.community_view.button_send_money.clicked.connect(self.open_transfer_money_dialog) - self.widget.centralWidget().layout().addWidget(self.community_view) - - @classmethod - def startup(cls, app): - qmainwindow = QMainWindow() - - main_window = cls(app, None, HomeScreenWidget(app, None), - CommunityWidget(app, None, None), - None, #NodeManager.create(qmainwindow), - qmainwindow, Ui_MainWindow(), - QLabel("", qmainwindow), QLabel("", qmainwindow), - QLabel("", qmainwindow), QComboBox(qmainwindow), - PasswordAskerDialog(None)) - app.version_requested.connect(main_window.latest_version_requested) - app.account_imported.connect(main_window.import_account_accepted) - app.account_changed.connect(main_window.change_account) - main_window._init_ui() - main_window._init_homescreen() - main_window._init_community_view() - - main_window.update_time() - if app.preferences['maximized']: - main_window.widget.showMaximized() - else: - main_window.widget.show() - if app.current_account: - main_window.password_asker = PasswordAskerDialog(app.current_account) - main_window.community_view.change_account(app.current_account, main_window.password_asker) - main_window.app.current_account.contacts_changed.connect(main_window.refresh_contacts) - main_window.refresh() - return main_window - - def change_account(self): - if self.account: - self.account.contacts_changed.disconnect(self.refresh_contacts) - self.account = self.app.current_account - self.password_asker.change_account(self.account) - self.community_view.change_account(self.account, self.password_asker) - if self.account: - self.account.contacts_changed.connect(self.refresh_contacts) - self.refresh() - - @asyncify - async def open_add_account_dialog(self, checked=False): - dialog = ProcessConfigureAccount(self.app, None) - result = await dialog.async_exec() - if result == QDialog.Accepted: - self.action_change_account(self.account.name) - - @asyncify - async def open_configure_account_dialog(self, checked=False): - dialog = ProcessConfigureAccount(self.app, self.account) - result = await dialog.async_exec() - if result == QDialog.Accepted: - if self.account: - self.action_change_account(self.account.name) - else: - self.refresh() - - @pyqtSlot(str) - def display_error(self, error): - QMessageBox.critical(self, ":(", - error, - QMessageBox.Ok) - - @pyqtSlot(int) - def referential_changed(self, index): - if self.account: - self.account.set_display_referential(index) - if self.community_view: - self.community_view.referential_changed() - self.homescreen.referential_changed() - - @pyqtSlot() - def update_time(self): - dateTime = QDateTime.currentDateTime() - self.label_time.setText("{0}".format(QLocale.toString( - QLocale(), - QDateTime.currentDateTime(), - QLocale.dateTimeFormat(QLocale(), QLocale.NarrowFormat) - ))) - timer = QTimer() - timer.timeout.connect(self.update_time) - timer.start(1000) - - @pyqtSlot() - def delete_contact(self): - contact = self.sender().data() - self.account.remove_contact(contact) - - @pyqtSlot() - def edit_contact(self): - index = self.sender().data() - dialog = ConfigureContactDialog.edit_contact(self.app, self.account, self.widget, index) - dialog.exec_() - - def action_change_account(self, account_name): - self.app.change_current_account(self.app.get_account(account_name)) - - @pyqtSlot() - def action_open_add_community(self): - dialog = ProcessConfigureCommunity(self.app, - self.account, None, - self.password_asker) - if dialog.exec_() == QDialog.Accepted: - self.app.save(self.account) - dialog.community.start_coroutines() - self.homescreen.refresh() - - def open_transfer_money_dialog(self): - dialog = TransferMoneyDialog(self.app, - self.account, - self.password_asker, - self.community_view.community, - None) - if dialog.exec() == QDialog.Accepted: - self.community_view.tab_history.table_history.model().sourceModel().refresh_transfers() - - def open_certification_dialog(self): - CertificationDialog.open_dialog(self.app, - self.account, - self.community_view.community, - self.password_asker) - - def open_revocation_dialog(self): - RevocationDialog.open_dialog(self.app, - self.account) - - def open_add_contact_dialog(self): - dialog = ConfigureContactDialog.new_contact(self.app, self.account, self.widget) - dialog.exec_() - - def open_preferences_dialog(self): - dialog = PreferencesDialog(self.app) - dialog.exec_() - - def open_about_popup(self): - """ - Open about popup window - """ - aboutDialog = QDialog(self.widget) - aboutUi = Ui_AboutPopup() - aboutUi.setupUi(aboutDialog) - - latest = self.app.available_version - version_info = "" - version_url = "" - if not latest[0]: - version_info = self.tr("Latest release : {version}") \ - .format(version=latest[1]) - version_url = latest[2] - - new_version_text = """ - <p><b>{version_info}</b></p> - <p><a href="{version_url}">{link_text}</a></p> - """.format(version_info=version_info, - version_url=version_url, - link_text=self.tr("Download link")) - else: - new_version_text = "" - - text = self.tr(""" - <h1>sakia</h1> - - <p>Python/Qt duniter client</p> - <p><a href="https://github.com/duniter/sakia">https://github.com/duniter/sakia</a></p> - - <p>Version : {:}</p> - {new_version_text} - - <p>License : GPLv3</p> - - <p><b>Authors</b></p> - - <p>inso</p> - <p>vit</p> - <p>Moul</p> - <p>canercandan</p> - """).format(__version__, new_version_text=new_version_text) - - aboutUi.label.setText(text) - aboutDialog.show() - - @pyqtSlot() - def latest_version_requested(self): - latest = self.app.available_version - logging.debug("Latest version requested") - if not latest[0]: - version_info = self.tr("Please get the latest release {version}") \ - .format(version=latest[1]) - version_url = latest[2] - - if self.app.preferences['notifications']: - toast.display("sakia", """{version_info}""".format( - version_info=version_info, - version_url=version_url)) - - @pyqtSlot(Community) - def change_community(self, community): - if community: - self.homescreen.hide() - self.community_view.show() - else: - self.community_view.hide() - self.homescreen.show() - - self.community_view.change_community(community) - - def refresh_accounts(self): - self.ui.menu_change_account.clear() - for account_name in sorted(self.app.accounts.keys()): - action = QAction(account_name, self.widget) - action.triggered.connect(lambda checked, account_name=account_name: self.action_change_account(account_name)) - self.ui.menu_change_account.addAction(action) - - def refresh_contacts(self): - self.ui.menu_contacts_list.clear() - if self.account: - for index, contact in enumerate(self.account.contacts): - contact_menu = self.ui.menu_contacts_list.addMenu(contact['name']) - edit_action = contact_menu.addAction(self.tr("Edit")) - edit_action.triggered.connect(self.edit_contact) - edit_action.setData(index) - delete_action = contact_menu.addAction(self.tr("Delete")) - delete_action.setData(contact) - delete_action.triggered.connect(self.delete_contact) - - @asyncify - async def open_duniter_ui(self, checked=False): - if not self.node_manager.widget.isVisible(): - self.node_manager.open_home_page() - - def refresh(self): - """ - Refresh main window - When the selected account changes, all the widgets - in the window have to be refreshed - """ - logging.debug("Refresh started") - self.refresh_accounts() - self.community_view.hide() - self.homescreen.show() - self.homescreen.refresh() - - if self.account is None: - self.widget.setWindowTitle(self.tr("sakia {0}").format(__version__)) - self.ui.action_add_a_contact.setEnabled(False) - self.ui.actionCertification.setEnabled(False) - self.ui.actionTransfer_money.setEnabled(False) - self.ui.action_configure_parameters.setEnabled(False) - self.ui.action_set_as_default.setEnabled(False) - self.ui.menu_contacts_list.setEnabled(False) - self.combo_referential.setEnabled(False) - self.status_label.setText(self.tr("")) - else: - self.combo_referential.blockSignals(True) - self.combo_referential.clear() - for ref in money.Referentials: - self.combo_referential.addItem(ref.translated_name()) - logging.debug(self.app.preferences) - - self.combo_referential.setEnabled(True) - self.combo_referential.blockSignals(False) - self.combo_referential.setCurrentIndex(self.app.preferences['ref']) - self.ui.action_add_a_contact.setEnabled(True) - self.ui.actionCertification.setEnabled(True) - self.ui.actionTransfer_money.setEnabled(True) - self.ui.menu_contacts_list.setEnabled(True) - self.ui.action_configure_parameters.setEnabled(True) - self.widget.setWindowTitle(self.tr("sakia {0} - Account : {1}").format(__version__, - self.account.name)) - - self.refresh_contacts() - - def import_account(self): - import_dialog = ImportAccountDialog(self.app, self.widget) - import_dialog.exec_() - - def import_account_accepted(self, account_name): - # open account after import - self.action_change_account(account_name) - - def export_account(self): - # Testable way of using a QFileDialog - export_dialog = QFileDialog(self.widget) - export_dialog.setObjectName('ExportFileDialog') - export_dialog.setWindowTitle(self.tr("Export an account")) - export_dialog.setNameFilter(self.tr("All account files (*.acc)")) - export_dialog.setLabelText(QFileDialog.Accept, self.tr('Export')) - export_dialog.setOption(QFileDialog.DontUseNativeDialog, True) - export_dialog.accepted.connect(self.export_account_accepted) - export_dialog.show() - - def export_account_accepted(self): - export_dialog = self.sender() - selected_file = export_dialog.selectedFiles() - if selected_file: - if selected_file[0][-4:] == ".acc": - path = selected_file[0] - else: - path = selected_file[0] + ".acc" - self.app.export_account(path, self.account) - - def eventFilter(self, target, event): - """ - Event filter on the widget - :param QObject target: the target of the event - :param QEvent event: the event - :return: bool - """ - if target == self.widget: - if event.type() == QEvent.LanguageChange: - self.ui.retranslateUi(self) - self.refresh() - return self.widget.eventFilter(target, event) - return False - diff --git a/src/sakia/gui/member.py b/src/sakia/gui/member.py index 5d2744dea1d3e3e9953e514727da816f423814bd..d81d312ccc11eadb7cb73292a38fac71b38992f2 100644 --- a/src/sakia/gui/member.py +++ b/src/sakia/gui/member.py @@ -6,7 +6,7 @@ from PyQt5.QtWidgets import QDialog, QWidget from ..core.graph import WoTGraph from .widgets.busy import Busy from ..tools.decorators import asyncify -from ..gen_resources.member_uic import Ui_MemberView +from ..presentation.member_uic import Ui_MemberView from ..tools.exceptions import MembershipNotFoundError, LookupFailureError, NoPeerAvailable from ..core.registry import LocalState @@ -25,7 +25,7 @@ class MemberDialog(QObject): :param sakia.core.community.Community community: Community instance :param sakia.core.registry.identity.Identity identity: Identity instance :param PyQt5.QtWidget widget: The class of the widget - :param sakia.gen_resources.member_uic.Ui_DialogMember ui: the class of the ui applyed to the widget + :param sakia.presentation.member_uic.Ui_DialogMember ui: the class of the ui applyed to the widget :return: """ super().__init__() diff --git a/src/sakia/gui/navigation/__init__.py b/src/sakia/gui/navigation/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/sakia/gui/navigation/controller.py b/src/sakia/gui/navigation/controller.py new file mode 100644 index 0000000000000000000000000000000000000000..3e94d470e1ea9bbdbe18c0d9fee31734ffdbfcd0 --- /dev/null +++ b/src/sakia/gui/navigation/controller.py @@ -0,0 +1,34 @@ +from PyQt5.QtWidgets import QTreeView +from .model import NavigationModel +from ..agent.controller import AgentController +from ..agent.model import AgentModel + + +class NavigationController(AgentController): + """ + The navigation panel + """ + + def __init__(self, parent, presentation, model): + """ + Constructor of the navigation agent + + :param PyQt5.QtWidgets.QTreeView presentation: the presentation + :param sakia.core.gui.navigation.model.NavigationModel model: the model + """ + super().__init__(parent, presentation, model) + + @classmethod + def create(cls, parent, app): + """ + Instanciate a navigation agent + :param sakia.gui.agent.controller.AgentController parent: + :return: a new Navigation controller + :rtype: NavigationController + """ + presentation = QTreeView(parent.presentation) + model = NavigationModel(None, app) + navigation = cls(presentation, model) + model.setParent(navigation) + return navigation + diff --git a/src/sakia/gui/navigation/model.py b/src/sakia/gui/navigation/model.py new file mode 100644 index 0000000000000000000000000000000000000000..bfe30093d7b51ad1f300470195de005489c2822f --- /dev/null +++ b/src/sakia/gui/navigation/model.py @@ -0,0 +1,75 @@ +from PyQt5.QtCore import QAbstractItemModel, Qt, QVariant + + +class NavigationModel(QAbstractItemModel): + """ + The model of Navigation agent + """ + def __init__(self, parent, app): + super().__init__(parent) + self.app = app + + def tree(self): + navigation = {'profile': { + 'node': { + 'title': self.account.name + }, + 'children': {} + } + } + for c in self.account.communities: + navigation['profile']['children'].append({ + 'node': { + 'title': c.currency + }, + 'children': { + 'transfers': { + 'node': { + 'title': self.tr('Transfers') + } + }, + 'network': { + 'node': { + 'title': self.tr('Network') + } + }, + 'network': { + 'node': { + 'title': self.tr('Network') + } + } + } + }) + + def rowCount(self, parent): + return len(self.nodes_data) + + def columnCount(self, parent): + return len(self.columns_types) + + def headerData(self, section, orientation, role): + if role != Qt.DisplayRole: + return QVariant() + + return self.columns_types[section] + + def data(self, index, role): + row = index.row() + col = index.column() + + if not index.isValid(): + return QVariant() + + node = self.nodes_data[row] + if role == Qt.DisplayRole: + return node[col] + if role == Qt.BackgroundColorRole: + return self.node_colors[node[self.columns_types.index('state')]] + if role == Qt.ToolTipRole: + return self.node_states[node[self.columns_types.index('state')]]() + + return QVariant() + + def flags(self, index): + return Qt.ItemIsSelectable | Qt.ItemIsEnabled + diff --git a/src/sakia/gui/network_tab.py b/src/sakia/gui/network_tab.py index ace7c59e9a5febad3462debcbb5a8735c9ebcc3a..95ba687e44af709c1ad61cddd41f832b60b7ce80 100644 --- a/src/sakia/gui/network_tab.py +++ b/src/sakia/gui/network_tab.py @@ -12,7 +12,7 @@ from PyQt5.QtWidgets import QWidget, QMenu, QAction from PyQt5.QtCore import Qt, QModelIndex, pyqtSlot, QUrl, QEvent from ..models.network import NetworkTableModel, NetworkFilterProxyModel from duniterpy.api import bma -from ..gen_resources.network_tab_uic import Ui_NetworkTabWidget +from ..presentation.network_tab_uic import Ui_NetworkTabWidget class NetworkTabWidget(QWidget, Ui_NetworkTabWidget): diff --git a/src/sakia/gui/node_manager.py b/src/sakia/gui/node_manager.py index 7dd8432442563ffc58754f7c52ccc8a2102f0ace..ffbe8067de8659fa33e933561c73b3589ca18e38 100644 --- a/src/sakia/gui/node_manager.py +++ b/src/sakia/gui/node_manager.py @@ -18,7 +18,7 @@ class NodeManager(QObject): Init MemberDialog :param PyQt5.QtWidget widget: The class of the widget - :param sakia.gen_resources.member_uic.Ui_DialogMember ui: the class of the ui applyed to the widget + :param sakia.presentation.member_uic.Ui_DialogMember ui: the class of the ui applyed to the widget :return: """ super().__init__() diff --git a/src/sakia/gui/password_asker.py b/src/sakia/gui/password_asker.py index be1b97c3a64a2e4e09ff76e638f8797f94aeb707..ecb926688e3f22bd70acf152ff3a0de6895c9ccc 100644 --- a/src/sakia/gui/password_asker.py +++ b/src/sakia/gui/password_asker.py @@ -10,7 +10,7 @@ import asyncio from PyQt5.QtCore import QEvent from PyQt5.QtWidgets import QDialog, QMessageBox -from ..gen_resources.password_asker_uic import Ui_PasswordAskerDialog +from ..presentation.password_asker_uic import Ui_PasswordAskerDialog class PasswordAskerDialog(QDialog, Ui_PasswordAskerDialog): diff --git a/src/sakia/gui/preferences.py b/src/sakia/gui/preferences.py index 6f04bc1d4a8d245fc525c6d6c646f871c3006c65..9d00fc555c37974bd95319c2603094111ee05f4c 100644 --- a/src/sakia/gui/preferences.py +++ b/src/sakia/gui/preferences.py @@ -8,7 +8,7 @@ from PyQt5.QtCore import QCoreApplication from PyQt5.QtWidgets import QDialog from ..core import money -from ..gen_resources.preferences_uic import Ui_PreferencesDialog +from ..presentation.preferences_uic import Ui_PreferencesDialog class PreferencesDialog(QDialog, Ui_PreferencesDialog): diff --git a/src/sakia/gui/process_cfg_account.py b/src/sakia/gui/process_cfg_account.py index 57cc4d54907b79b3de173289cffcb13ddaa7926b..023d726dcd8e33294a4dd74aae61443006f07c71 100644 --- a/src/sakia/gui/process_cfg_account.py +++ b/src/sakia/gui/process_cfg_account.py @@ -6,7 +6,7 @@ Created on 6 mars 2014 import logging import asyncio from duniterpy.key import SigningKey -from ..gen_resources.account_cfg_uic import Ui_AccountConfigurationDialog +from ..presentation.account_cfg_uic import Ui_AccountConfigurationDialog from ..gui.process_cfg_community import ProcessConfigureCommunity from ..gui.password_asker import PasswordAskerDialog, detect_non_printable from ..gui.widgets.dialogs import QAsyncMessageBox diff --git a/src/sakia/gui/process_cfg_community.py b/src/sakia/gui/process_cfg_community.py index 5717579fb5dac384cc8501a94bd48e9f9578c64e..4f437e380c7ffba363b84030aab166dac87bbddd 100644 --- a/src/sakia/gui/process_cfg_community.py +++ b/src/sakia/gui/process_cfg_community.py @@ -15,7 +15,7 @@ from PyQt5.QtWidgets import QDialog, QMenu, QApplication from PyQt5.QtGui import QCursor from PyQt5.QtCore import pyqtSignal, QObject -from ..gen_resources.community_cfg_uic import Ui_CommunityConfigurationDialog +from ..presentation.community_cfg_uic import Ui_CommunityConfigurationDialog from ..models.peering import PeeringTreeModel from ..core import Community from ..core.net import Node diff --git a/src/sakia/gui/revocation.py b/src/sakia/gui/revocation.py index 5de30bf1ef1e64d9a3601ad49fb4340e345da408..31b3d6b31beb8d14df7a67f1372334eb5bf3911b 100644 --- a/src/sakia/gui/revocation.py +++ b/src/sakia/gui/revocation.py @@ -11,7 +11,7 @@ from PyQt5.QtCore import QObject from .widgets.dialogs import QAsyncMessageBox from ..tools.decorators import asyncify -from ..gen_resources.revocation_uic import Ui_RevocationDialog +from ..presentation.revocation_uic import Ui_RevocationDialog class RevocationDialog(QObject): diff --git a/src/sakia/gui/status_bar/__init__.py b/src/sakia/gui/status_bar/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/sakia/gui/status_bar/controller.py b/src/sakia/gui/status_bar/controller.py new file mode 100644 index 0000000000000000000000000000000000000000..24c1d7062ddf566a66965f2fb54ed6ac13464206 --- /dev/null +++ b/src/sakia/gui/status_bar/controller.py @@ -0,0 +1,76 @@ +from PyQt5.QtCore import QLocale, pyqtSlot, QDateTime, QTimer +from ..agent.controller import AgentController +from .model import StatusBarModel +from .view import StatusBarView +import logging + + +class StatusBarController(AgentController): + """ + The navigation panel + """ + + def __init__(self, parent, view, model): + """ + Constructor of the navigation agent + + :param sakia.gui.status_bar.view.StatusBarView view: the presentation + :param sakia.core.status_bar.model.StatusBarModel model: the model + """ + super().__init__(parent, view, model) + view.combo_referential.currentIndexChanged[int].connect(self.referential_changed) + self.update_time() + + @classmethod + def create(cls, parent, app): + """ + Instanciate a navigation agent + :param sakia.gui.main_window.controller.MainWindowController parent: + :return: a new Navigation controller + :rtype: NavigationController + """ + view = StatusBarView(parent.view) + + model = StatusBarModel(None, app) + status_bar = cls(parent, view, model) + model.setParent(status_bar) + return status_bar + + @pyqtSlot() + def update_time(self): + dateTime = QDateTime.currentDateTime() + self.view.label_time.setText("{0}".format(QLocale.toString( + QLocale(), + QDateTime.currentDateTime(), + QLocale.dateTimeFormat(QLocale(), QLocale.NarrowFormat) + ))) + timer = QTimer() + timer.timeout.connect(self.update_time) + timer.start(1000) + + def refresh(self): + """ + Refresh main window + When the selected account changes, all the widgets + in the window have to be refreshed + """ + logging.debug("Refresh started") + + if self.model.account is None: + self.view.combo_referential.setEnabled(False) + self.view.status_label.setText(self.tr("")) + else: + self.view.combo_referential.blockSignals(True) + self.view.combo_referential.clear() + for ref in self.model.referentials(): + self.view.combo_referential.addItem(ref.translated_name()) + logging.debug(self.app.preferences) + + self.view.combo_referential.setEnabled(True) + self.view.combo_referential.blockSignals(False) + self.view.combo_referential.setCurrentIndex(self.app.preferences['ref']) + + @pyqtSlot(int) + def referential_changed(self, index): + if self.model.account: + self.model.account.set_display_referential(index) \ No newline at end of file diff --git a/src/sakia/gui/status_bar/model.py b/src/sakia/gui/status_bar/model.py new file mode 100644 index 0000000000000000000000000000000000000000..b1ef970936ea787d9b1c6adaba2f267880940f30 --- /dev/null +++ b/src/sakia/gui/status_bar/model.py @@ -0,0 +1,11 @@ +from sakia.gui.agent.model import AgentModel + + +class StatusBarModel(AgentModel): + """ + The model of Navigation agent + """ + + def __init__(self, parent, app): + super().__init__(parent) + self.app = app diff --git a/src/sakia/gui/status_bar/view.py b/src/sakia/gui/status_bar/view.py new file mode 100644 index 0000000000000000000000000000000000000000..5f631e69bdcdaf93df00beb6eca243608e9176ce --- /dev/null +++ b/src/sakia/gui/status_bar/view.py @@ -0,0 +1,26 @@ +from PyQt5.QtWidgets import QStatusBar +from PyQt5.QtWidgets import QLabel, QComboBox +from PyQt5.QtCore import Qt + + +class StatusBarView(QStatusBar): + """ + The model of Navigation agent + """ + + def __init__(self, parent): + super().__init__(parent) + self.label_icon = QLabel("", parent) + + self.status_label = QLabel("", parent) + self.status_label.setTextFormat(Qt.RichText) + + self.label_time = QLabel("", parent) + + self.combo_referential = QComboBox(parent) + self.combo_referential.setEnabled(False) + + self.addPermanentWidget(self.label_icon, 1) + self.addPermanentWidget(self.status_label, 2) + self.addPermanentWidget(self.label_time) + self.addPermanentWidget(self.combo_referential) \ No newline at end of file diff --git a/src/sakia/gui/toolbar/__init__.py b/src/sakia/gui/toolbar/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/sakia/gui/toolbar/controller.py b/src/sakia/gui/toolbar/controller.py new file mode 100644 index 0000000000000000000000000000000000000000..78b4ab0bfa7368c0457ba5b23e5ebaaeb31a3618 --- /dev/null +++ b/src/sakia/gui/toolbar/controller.py @@ -0,0 +1,254 @@ +from PyQt5.QtWidgets import QFrame, QAction, QMenu, QDialog, QMessageBox +from PyQt5.QtGui import QIcon +from PyQt5.QtCore import QObject, QT_TRANSLATE_NOOP, Qt +from .model import ToolbarModel +from .view import ToolbarView +from .toolbar_uic import Ui_SakiaToolbar +from ...tools.decorators import asyncify, once_at_a_time, cancel_once_task +from ..widgets.dialogs import QAsyncMessageBox, QAsyncFileDialog, dialog_async_exec +from ..widgets import toast +import logging + + +class ToolbarController(QObject): + """ + The navigation panel + """ + _action_showinfo_text = QT_TRANSLATE_NOOP("CommunityWidget", "Show informations") + _action_explore_text = QT_TRANSLATE_NOOP("CommunityWidget", "Explore the Web of Trust") + _action_publish_uid_text = QT_TRANSLATE_NOOP("CommunityWidget", "Publish UID") + _action_revoke_uid_text = QT_TRANSLATE_NOOP("CommunityWidget", "Revoke UID") + + def __init__(self, view, model, account, community, password_asker): + """ + + :param sakia.gui.toolbar.view.ToolbarView view: + :param sakia.gui.toolbar.model.ToolbarModel model: + """ + self.view = view + self.model = model + self.account = account + self.community = community + self.password_asker = password_asker + + tool_menu = QMenu(self.tr("Tools"), self.toolbutton_menu) + self.toolbutton_menu.setMenu(tool_menu) + + self.action_publish_uid = QAction(self.tr(ToolbarController._action_publish_uid_text), self) + self.action_revoke_uid = QAction(self.tr(ToolbarController._action_revoke_uid_text), self) + self.action_showinfo = QAction(self.tr(ToolbarController._action_showinfo_text), self) + self.action_explorer = QAction(self.tr(ToolbarController._action_explore_text), self) + + action_showinfo = QAction(self.tr("Show informations"), self.toolbutton_menu) + action_showinfo.triggered.connect(lambda: self.show_closable_tab(self.tab_informations, + QIcon(":/icons/informations_icon"), + self.tr("Informations"))) + tool_menu.addAction(action_showinfo) + + action_showexplorer = QAction(self.tr("Show explorer"), self.toolbutton_menu) + action_showexplorer.triggered.connect(lambda: self.show_closable_tab(self.tab_explorer.widget, + QIcon(":/icons/explorer_icon"), + self.tr("Explorer"))) + tool_menu.addAction(action_showexplorer) + + menu_advanced = QMenu(self.tr("Advanced"), self.toolbutton_menu) + action_gen_revokation = QAction(self.tr("Save revokation document"), menu_advanced) + action_gen_revokation.triggered.connect(self.action_save_revokation) + menu_advanced.addAction(action_gen_revokation) + tool_menu.addMenu(menu_advanced) + + self.action_publish_uid.triggered.connect(self.publish_uid) + tool_menu.addAction(self.action_publish_uid) + + self.button_membership.clicked.connect(self.send_membership_demand) + + self.community_view.button_certification.clicked.connect(self.open_certification_dialog) + self.community_view.button_send_money.clicked.connect(self.open_transfer_money_dialog) + + @classmethod + def create(cls, parent): + """ + Instanciate a navigation agent + :param sakia.gui.agent.controller.AgentController parent: + :return: a new Toolbar controller + :rtype: ToolbarController + """ + view = ToolbarView(parent.view) + model = ToolbarModel(None) + toolbar = cls(view, model) + model.setParent(toolbar) + return toolbar + + def cancel_once_tasks(self): + cancel_once_task(self, self.refresh_block) + cancel_once_task(self, self.refresh_status) + logging.debug("Cancelled status") + cancel_once_task(self, self.refresh_quality_buttons) + + def change_account(self, account, password_asker): + self.cancel_once_tasks() + self.account = account + self.password_asker = password_asker + + def change_community(self, community): + self.cancel_once_tasks() + + logging.debug("Changed community to {0}".format(community)) + self.button_membership.setText(self.tr("Membership")) + self.button_membership.setEnabled(False) + self.button_certification.setEnabled(False) + self.action_publish_uid.setEnabled(False) + self.community = community + self.refresh_quality_buttons() + + @asyncify + async def action_save_revokation(self, checked=False): + password = await self.password_asker.async_exec() + if self.password_asker.result() == QDialog.Rejected: + return + + raw_document = await self.account.generate_revokation(self.community, password) + # Testable way of using a QFileDialog + selected_files = await QAsyncFileDialog.get_save_filename(self, self.tr("Save a revokation document"), + "", self.tr("All text files (*.txt)")) + if selected_files: + path = selected_files[0] + if not path.endswith('.txt'): + path = "{0}.txt".format(path) + with open(path, 'w') as save_file: + save_file.write(raw_document) + + dialog = QMessageBox(QMessageBox.Information, self.tr("Revokation file"), + self.tr("""<div>Your revokation document has been saved.</div> +<div><b>Please keep it in a safe place.</b></div> +The publication of this document will remove your identity from the network.</p>"""), QMessageBox.Ok, + self) + dialog.setTextFormat(Qt.RichText) + await dialog_async_exec(dialog) + + @asyncify + async def send_membership_demand(self, checked=False): + password = await self.password_asker.async_exec() + if self.password_asker.result() == QDialog.Rejected: + return + result = await self.account.send_membership(password, self.community, 'IN') + if result[0]: + if self.app.preferences['notifications']: + toast.display(self.tr("Membership"), self.tr("Success sending Membership demand")) + else: + await QAsyncMessageBox.information(self, self.tr("Membership"), + self.tr("Success sending Membership demand")) + else: + if self.app.preferences['notifications']: + toast.display(self.tr("Membership"), result[1]) + else: + await QAsyncMessageBox.critical(self, self.tr("Membership"), + result[1]) + + @asyncify + async def send_membership_leaving(self): + reply = await QAsyncMessageBox.warning(self, self.tr("Warning"), + self.tr("""Are you sure ? +Sending a leaving demand cannot be canceled. +The process to join back the community later will have to be done again.""") +.format(self.account.pubkey), QMessageBox.Ok | QMessageBox.Cancel) + if reply == QMessageBox.Ok: + password = self.password_asker.exec_() + if self.password_asker.result() == QDialog.Rejected: + return + result = await self.account.send_membership(password, self.community, 'OUT') + if result[0]: + if self.app.preferences['notifications']: + toast.display(self.tr("Revoke"), self.tr("Success sending Revoke demand")) + else: + await QAsyncMessageBox.information(self, self.tr("Revoke"), + self.tr("Success sending Revoke demand")) + else: + if self.app.preferences['notifications']: + toast.display(self.tr("Revoke"), result[1]) + else: + await QAsyncMessageBox.critical(self, self.tr("Revoke"), + result[1]) + + @asyncify + async def publish_uid(self, checked=False): + password = await self.password_asker.async_exec() + if self.password_asker.result() == QDialog.Rejected: + return + result = await self.account.send_selfcert(password, self.community) + if result[0]: + if self.app.preferences['notifications']: + toast.display(self.tr("UID"), self.tr("Success publishing your UID")) + else: + await QAsyncMessageBox.information(self, self.tr("Membership"), + self.tr("Success publishing your UID")) + else: + if self.app.preferences['notifications']: + toast.display(self.tr("UID"), result[1]) + else: + await QAsyncMessageBox.critical(self, self.tr("UID"), + result[1]) + + @once_at_a_time + @asyncify + async def refresh_quality_buttons(self): + if self.account and self.community: + try: + account_identity = await self.account.identity(self.community) + published_uid = await account_identity.published_uid(self.community) + uid_is_revokable = await account_identity.uid_is_revokable(self.community) + if published_uid: + logging.debug("UID Published") + self.action_revoke_uid.setEnabled(uid_is_revokable) + is_member = await account_identity.is_member(self.community) + if is_member: + self.button_membership.setText(self.tr("Renew membership")) + self.button_membership.setEnabled(True) + self.button_certification.setEnabled(True) + self.action_publish_uid.setEnabled(False) + else: + logging.debug("Not a member") + self.button_membership.setText(self.tr("Send membership demand")) + self.button_membership.setEnabled(True) + self.action_publish_uid.setEnabled(False) + if await self.community.get_block(0) is not None: + self.button_certification.setEnabled(False) + else: + logging.debug("UID not published") + self.button_membership.setEnabled(False) + self.button_certification.setEnabled(False) + self.action_publish_uid.setEnabled(True) + except LookupFailureError: + self.button_membership.setEnabled(False) + self.button_certification.setEnabled(False) + self.action_publish_uid.setEnabled(False) + + def open_certification_dialog(self): + CertificationDialog.open_dialog(self.app, + self.account, + self.community_view.community, + self.password_asker) + + def open_revocation_dialog(self): + RevocationDialog.open_dialog(self.app, + self.account) + + def open_transfer_money_dialog(self): + dialog = TransferMoneyDialog(self.app, + self.account, + self.password_asker, + self.community_view.community, + None) + if dialog.exec() == QDialog.Accepted: + self.community_view.tab_history.table_history.model().sourceModel().refresh_transfers() + + def retranslateUi(self, widget): + """ + Method to complete translations missing from generated code + :param widget: + :return: + """ + self.action_publish_uid.setText(self.tr(ToolbarController.action_publish_uid_text)) + self.action_revoke_uid.setText(self.tr(ToolbarController.action_revoke_uid_text)) + self.action_showinfo.setText(self.tr(ToolbarController.action_showinfo_text)) + super().retranslateUi(self) diff --git a/src/sakia/gui/toolbar/model.py b/src/sakia/gui/toolbar/model.py new file mode 100644 index 0000000000000000000000000000000000000000..5e6250e96c61d883494e47c8b6f0daffbd69e666 --- /dev/null +++ b/src/sakia/gui/toolbar/model.py @@ -0,0 +1,10 @@ +from sakia.gui.agent.model import AgentModel + + +class ToolbarModel(AgentModel): + """ + The model of Navigation agent + """ + + def __init__(self, parent): + super().__init__(parent) diff --git a/src/sakia/gui/toolbar/toolbar.ui b/src/sakia/gui/toolbar/toolbar.ui new file mode 100644 index 0000000000000000000000000000000000000000..b90226b937cf566c22bb009f306d835036056ef5 --- /dev/null +++ b/src/sakia/gui/toolbar/toolbar.ui @@ -0,0 +1,119 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>SakiaToolbar</class> + <widget class="QFrame" name="SakiaToolbar"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>690</width> + <height>240</height> + </rect> + </property> + <property name="windowTitle"> + <string>Frame</string> + </property> + <property name="frameShape"> + <enum>QFrame::StyledPanel</enum> + </property> + <property name="frameShadow"> + <enum>QFrame::Raised</enum> + </property> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <widget class="QPushButton" name="button_send_money"> + <property name="text"> + <string>Send money</string> + </property> + <property name="icon"> + <iconset resource="../icons/icons.qrc"> + <normaloff>:/icons/payment_icon</normaloff>:/icons/payment_icon</iconset> + </property> + <property name="iconSize"> + <size> + <width>32</width> + <height>32</height> + </size> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="button_certification"> + <property name="text"> + <string>Certification</string> + </property> + <property name="icon"> + <iconset resource="../icons/icons.qrc"> + <normaloff>:/icons/certification_icon</normaloff>:/icons/certification_icon</iconset> + </property> + <property name="iconSize"> + <size> + <width>32</width> + <height>32</height> + </size> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="button_membership"> + <property name="text"> + <string>Renew membership</string> + </property> + <property name="icon"> + <iconset resource="../icons/icons.qrc"> + <normaloff>:/icons/renew_membership</normaloff>:/icons/renew_membership</iconset> + </property> + <property name="iconSize"> + <size> + <width>32</width> + <height>32</height> + </size> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>169</width> + <height>221</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QToolButton" name="toolbutton_menu"> + <property name="text"> + <string/> + </property> + <property name="icon"> + <iconset resource="../icons/icons.qrc"> + <normaloff>:/icons/menu_icon</normaloff>:/icons/menu_icon</iconset> + </property> + <property name="iconSize"> + <size> + <width>32</width> + <height>32</height> + </size> + </property> + <property name="popupMode"> + <enum>QToolButton::InstantPopup</enum> + </property> + <property name="autoRaise"> + <bool>false</bool> + </property> + <property name="arrowType"> + <enum>Qt::NoArrow</enum> + </property> + </widget> + </item> + </layout> + </widget> + <resources> + <include location="../icons/icons.qrc"/> + </resources> + <connections/> +</ui> diff --git a/src/sakia/gui/toolbar/view.py b/src/sakia/gui/toolbar/view.py new file mode 100644 index 0000000000000000000000000000000000000000..99787a63fa983113c1059e530c4923824b7c910a --- /dev/null +++ b/src/sakia/gui/toolbar/view.py @@ -0,0 +1,12 @@ +from PyQt5.QtWidgets import QFrame +from .toolbar_uic import Ui_SakiaToolbar + + +class ToolbarView(QFrame, Ui_SakiaToolbar): + """ + The model of Navigation agent + """ + + def __init__(self, parent): + super().__init__(parent) + self.setupUi(self) diff --git a/src/sakia/gui/transactions_tab.py b/src/sakia/gui/transactions_tab.py index a498c54944e80d7fa78313ad712d9e67dd26783c..656b855b7658946c130d81882301e1c48b522144 100644 --- a/src/sakia/gui/transactions_tab.py +++ b/src/sakia/gui/transactions_tab.py @@ -5,7 +5,7 @@ from PyQt5.QtWidgets import QWidget, QAbstractItemView, QHeaderView from PyQt5.QtCore import Qt, QObject, QDateTime, QTime, QModelIndex, pyqtSignal, pyqtSlot, QEvent from PyQt5.QtGui import QCursor -from ..gen_resources.transactions_tab_uic import Ui_transactionsTabWidget +from ..presentation.transactions_tab_uic import Ui_transactionsTabWidget from ..models.txhistory import HistoryTableModel, TxFilterProxyModel from ..tools.exceptions import NoPeerAvailable from ..tools.decorators import asyncify, once_at_a_time, cancel_once_task diff --git a/src/sakia/gui/transfer.py b/src/sakia/gui/transfer.py index 80fe8b4d6d05854e9f505ea1d1d10f4729645d59..90a07f08822ad102b97e95a926fec53a4ff90df8 100644 --- a/src/sakia/gui/transfer.py +++ b/src/sakia/gui/transfer.py @@ -11,7 +11,7 @@ from PyQt5.QtCore import QRegExp, Qt, QObject from PyQt5.QtGui import QRegExpValidator -from ..gen_resources.transfer_uic import Ui_TransferMoneyDialog +from ..presentation.transfer_uic import Ui_TransferMoneyDialog from .widgets import toast from .widgets.dialogs import QAsyncMessageBox, QMessageBox from ..tools.decorators import asyncify diff --git a/src/sakia/gui/widgets/search_user.py b/src/sakia/gui/widgets/search_user.py index 6f4a0195e13294bdb5ca850ec9ee2667a8df15d3..21860415ac5cf5e67bf5d29e284d96fd9aa5eb7a 100644 --- a/src/sakia/gui/widgets/search_user.py +++ b/src/sakia/gui/widgets/search_user.py @@ -8,7 +8,7 @@ from duniterpy.api import bma, errors from ...tools.decorators import asyncify from ...tools.exceptions import NoPeerAvailable from ...core.registry import BlockchainState, Identity -from ...gen_resources.search_user_view_uic import Ui_SearchUserWidget +from ...presentation.search_user_view_uic import Ui_SearchUserWidget class SearchUserWidget(QWidget, Ui_SearchUserWidget): diff --git a/src/sakia/gui/widgets/toast.py b/src/sakia/gui/widgets/toast.py index fd50cf8f8edcb900778f0b2f87c776ba1d4cad78..2e1aa124b01cc0a594796c1616441006a72a4ba7 100644 --- a/src/sakia/gui/widgets/toast.py +++ b/src/sakia/gui/widgets/toast.py @@ -8,7 +8,7 @@ import logging from PyQt5.QtCore import Qt, QThread from PyQt5.QtWidgets import QMainWindow, QApplication from PyQt5.QtGui import QImage, QPixmap -from ...gen_resources.toast_uic import Ui_Toast +from ...presentation.toast_uic import Ui_Toast window = None # global diff --git a/src/sakia/main.py b/src/sakia/main.py index 6e55dbf2bd773098c02a18ba0839864ca7a079b8..5e1c15803b334721788b770bd65fb2bd59b78f38 100755 --- a/src/sakia/main.py +++ b/src/sakia/main.py @@ -20,7 +20,7 @@ import PyQt5.QtSvg from quamash import QSelectorEventLoop from PyQt5.QtWidgets import QApplication, QMessageBox from PyQt5.QtCore import Qt -from sakia.gui.mainwindow import MainWindow +from sakia.gui.main_window.controller import MainWindowController from sakia.core.app import Application @@ -97,7 +97,7 @@ if __name__ == '__main__': with loop: app = Application.startup(sys.argv, sakia, loop) - window = MainWindow.startup(app) + window = MainWindowController.startup(app) loop.run_forever() try: loop.set_exception_handler(None) diff --git a/src/sakia/models/generic_tree.py b/src/sakia/models/generic_tree.py new file mode 100644 index 0000000000000000000000000000000000000000..4703d03b9ff3ddeaa8acc55c06da51fd6e07b95d --- /dev/null +++ b/src/sakia/models/generic_tree.py @@ -0,0 +1,154 @@ +""" +Created on 5 févr. 2014 + +@author: inso +""" + +from PyQt5.QtCore import QAbstractItemModel, QModelIndex, Qt +from PyQt5.QtGui import QIcon +import logging + + +class NodeItem(QAbstractItemModel): + def __init__(self, node, parent_item): + super().__init__(parent_item) + self.children = [] + self.node = node + self.parent_item = parent_item + + def appendChild(self, node_item): + self.children.append(node_item) + + def child(self, row): + return self.children[row] + + def childCount(self): + return len(self.children) + + def columnCount(self): + return 1 + + def data(self, index, role): + if role == Qt.DisplayRole and 'title' in self.node: + return self.node['title'] + + if role == Qt.ToolTipRole and 'tooltip' in self.node: + return self.node['tooltip'] + + if role == Qt.DecorationRole and 'icon' in self.node: + return QIcon(self.node['icon']) + + def row(self): + if self.parent_item: + return self.parent_item.row() + self.parent_item.children.index(self) + return 0 + + +class GenericTreeModel(QAbstractItemModel): + + """ + A Qt abstract item model to display nodes from a dict + + + dict_format = { + 'root_node': { + 'node': ["title", "icon", "tooltip", "action"], + 'children': {} + } + } + + """ + + def __init__(self, title, root_item): + """ + Constructor + """ + super().__init__(None) + self.title = title + self.root_item = root_item + + @classmethod + def create(cls, title, data): + def parse_node(node_data, parent_item): + node = NodeItem(node_data['node'], parent_item) + if parent_item: + parent_item.appendChild(node) + if 'children' in node_data: + for child in node_data['children']: + parse_node(child, node) + return node + + root_item = NodeItem({}, None) + for node in data: + parse_node(node, root_item) + + return cls(title, root_item) + + def columnCount(self, parent): + return 1 + + def data(self, index, role): + if not index.isValid(): + return None + + item = index.internalPointer() + + if role == Qt.DisplayRole and index.column() == 0: + return item.data(0, role) + + return None + + def flags(self, index): + if not index.isValid(): + return Qt.NoItemFlags + + if index.column() == 0: + return Qt.ItemIsEnabled | Qt.ItemIsSelectable + + def headerData(self, section, orientation, role): + if orientation == Qt.Horizontal \ + and role == Qt.DisplayRole and section == 0: + return self.title + return None + + def index(self, row, column, parent): + if not self.hasIndex(row, column, parent): + return QModelIndex() + + if not parent.isValid(): + parent_item = self.root_item + else: + parent_item = parent.internalPointer() + + child_item = parent_item.child(row) + if child_item: + return self.createIndex(row, column, child_item) + else: + return QModelIndex() + + def parent(self, index): + if not index.isValid(): + return QModelIndex() + + child_item = index.internalPointer() + parent_item = child_item.parent() + + if parent_item == self.root_item: + return QModelIndex() + + return self.createIndex(parent_item.row(), 0, parent_item) + + def rowCount(self, parent): + if parent.column() > 0: + return 0 + + if not parent.isValid(): + parent_item = self.root_item + else: + parent_item = parent.internalPointer() + + return parent_item.childCount() + + def setData(self, index, value, role=Qt.EditRole): + if index.column() == 0: + return True diff --git a/src/sakia/tests/unit/gui/test_generic_tree.py b/src/sakia/tests/unit/gui/test_generic_tree.py new file mode 100644 index 0000000000000000000000000000000000000000..d26ebc96bbab00cafb8c2fd0e58c442c29a33236 --- /dev/null +++ b/src/sakia/tests/unit/gui/test_generic_tree.py @@ -0,0 +1,66 @@ +import unittest +from unittest.mock import patch, MagicMock, Mock, PropertyMock +from PyQt5.QtCore import QLocale +from sakia.tests import QuamashTest +from sakia.models.generic_tree import GenericTreeModel +from PyQt5.QtWidgets import QTreeView, QDialog + + +class TestGenericTree(unittest.TestCase, QuamashTest): + def setUp(self): + self.setUpQuamash() + QLocale.setDefault(QLocale("en_GB")) + + def tearDown(self): + self.tearDownQuamash() + + def test_generic_tree(self): + data = [ + { + 'node': { + 'title': "Default Profile" + }, + 'children': [ + { + 'node': { + 'title': "Test net (inso)" + }, + 'children': [ + { + 'node': { + 'title': "Transactions" + }, + 'children': [] + }, + { + 'node': { + 'title': "Network" + }, + 'children': [] + } + ] + }, + { + 'node': { + 'title': "Le sou" + }, + 'children': [ + { + 'node': { + 'title': "Transactions" + }, + 'children': {} + }, + { + 'node': { + 'title': "Network" + }, + 'children': { + } + } + ] + } + ], + } + ] + tree_model = GenericTreeModel.create("Test", data) \ No newline at end of file diff --git a/src/sakia/tests/unit/gui/test_main_window.py b/src/sakia/tests/unit/gui/test_main_window.py index 0227edd5086c265d2ca1846af40cd21b8c68238c..b9ac850b1b972d7160c04fd7b5b3cfc2e924de75 100644 --- a/src/sakia/tests/unit/gui/test_main_window.py +++ b/src/sakia/tests/unit/gui/test_main_window.py @@ -42,7 +42,7 @@ class TestMainWindow(unittest.TestCase, QuamashTest): def test_change_account(self): widget = Mock(spec='PyQt5.QtWidgets.QMainWindow', create=True) widget.installEventFilter = Mock() - ui = Mock(spec='sakia.gen_resources.mainwindow_uic.Ui_MainWindow', create=True) + ui = Mock(spec='sakia.presentation.mainwindow_uic.Ui_MainWindow', create=True) ui.setupUi = Mock() label_icon = Mock() label_status = Mock() @@ -67,7 +67,7 @@ class TestMainWindow(unittest.TestCase, QuamashTest): def test_change_account_from_none(self): widget = Mock(spec='PyQt5.QtWidgets.QMainWindow', create=True) widget.installEventFilter = Mock() - ui = Mock(spec='sakia.gen_resources.mainwindow_uic.Ui_MainWindow', create=True) + ui = Mock(spec='sakia.presentation.mainwindow_uic.Ui_MainWindow', create=True) ui.setupUi = Mock() label_icon = Mock() label_status = Mock()