class ElectrumGui(Logger):

    network_dialog: Optional['NetworkDialog']

    @profiler
    def __init__(self, config: 'SimpleConfig', daemon: 'Daemon',
                 plugins: 'Plugins'):
        set_language(config.get('language', get_default_language()))
        Logger.__init__(self)
        self.logger.info(
            f"Qt GUI starting up... Qt={QtCore.QT_VERSION_STR}, PyQt={QtCore.PYQT_VERSION_STR}"
        )
        # Uncomment this call to verify objects are being properly
        # GC-ed when windows are closed
        #network.add_jobs([DebugMem([Abstract_Wallet, SPV, Synchronizer,
        #                            ElectrumWindow], interval=5)])
        QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_X11InitThreads)
        if hasattr(QtCore.Qt, "AA_ShareOpenGLContexts"):
            QtCore.QCoreApplication.setAttribute(
                QtCore.Qt.AA_ShareOpenGLContexts)
        if hasattr(QGuiApplication, 'setDesktopFileName'):
            QGuiApplication.setDesktopFileName('electrum-dash.desktop')
        self.gui_thread = threading.current_thread()
        self.config = config
        self.daemon = daemon
        self.plugins = plugins
        self.windows = []  # type: List[ElectrumWindow]
        self.efilter = OpenFileEventFilter(self.windows)
        self.app = QElectrumApplication(sys.argv)
        self.app.installEventFilter(self.efilter)
        self.app.setWindowIcon(read_QIcon("electrum-dash.png"))
        self._cleaned_up = False
        # timer
        self.timer = QTimer(self.app)
        self.timer.setSingleShot(False)
        self.timer.setInterval(500)  # msec

        self.network_dialog = None
        self.dash_net_dialog = None
        self.network_updated_signal_obj = QNetworkUpdatedSignalObject()
        self.dash_net_sobj = QDashNetSignalsObject()
        self._num_wizards_in_progress = 0
        self._num_wizards_lock = threading.Lock()
        self.dark_icon = self.config.get("dark_icon", False)
        self.tray = None
        self._init_tray()
        self.app.new_window_signal.connect(self.start_new_window)
        self.set_dark_theme_if_needed()
        run_hook('init_qt', self)

    def _init_tray(self):
        self.tray = QSystemTrayIcon(self.tray_icon(), None)
        self.tray.setToolTip('Dash Electrum')
        self.tray.activated.connect(self.tray_activated)
        self.build_tray_menu()
        self.tray.show()

    def set_dark_theme_if_needed(self):
        use_dark_theme = self.config.get('qt_gui_color_theme',
                                         'default') == 'dark'
        self.app.setStyle('Fusion')
        if use_dark_theme:
            from .dark_dash_style import dash_stylesheet
            self.app.setStyleSheet(dash_stylesheet)
        else:
            from .dash_style import dash_stylesheet
            self.app.setStyleSheet(dash_stylesheet)
        # Apply any necessary stylesheet patches
        patch_qt_stylesheet(use_dark_theme=use_dark_theme)
        # Even if we ourselves don't set the dark theme,
        # the OS/window manager/etc might set *a dark theme*.
        # Hence, try to choose colors accordingly:
        ColorScheme.update_from_widget(QWidget(), force_dark=use_dark_theme)

    def build_tray_menu(self):
        if not self.tray:
            return
        # Avoid immediate GC of old menu when window closed via its action
        if self.tray.contextMenu() is None:
            m = QMenu()
            self.tray.setContextMenu(m)
        else:
            m = self.tray.contextMenu()
            m.clear()
        m.addAction(_("Network"), self.show_network_dialog)
        for window in self.windows:
            name = window.wallet.basename()
            submenu = m.addMenu(name)
            submenu.addAction(_("Show/Hide"), window.show_or_hide)
            submenu.addAction(_("Close"), window.close)
        m.addAction(_("Dark/Light"), self.toggle_tray_icon)
        m.addSeparator()
        m.addAction(_("Exit Dash Electrum"), self.app.quit)

    def tray_icon(self):
        if self.dark_icon:
            return read_QIcon('electrum_dark_icon.png')
        else:
            return read_QIcon('electrum_light_icon.png')

    def toggle_tray_icon(self):
        if not self.tray:
            return
        self.dark_icon = not self.dark_icon
        self.config.set_key("dark_icon", self.dark_icon, True)
        self.tray.setIcon(self.tray_icon())

    def tray_activated(self, reason):
        if reason == QSystemTrayIcon.DoubleClick:
            if all([w.is_hidden() for w in self.windows]):
                for w in self.windows:
                    w.bring_to_top()
            else:
                for w in self.windows:
                    w.hide()

    def _cleanup_before_exit(self):
        if self._cleaned_up:
            return
        self._cleaned_up = True
        self.app.new_window_signal.disconnect()
        self.efilter = None
        # If there are still some open windows, try to clean them up.
        for window in list(self.windows):
            window.close()
            window.clean_up()
        if self.network_dialog:
            self.network_dialog.close()
            self.network_dialog.clean_up()
            self.network_dialog = None
        self.network_updated_signal_obj = None
        if self.dash_net_dialog:
            self.dash_net_dialog.close()
            self.dash_net_dialog.clean_up()
            self.dash_net_dialog = None
        self.dash_net_sobj = None
        # Shut down the timer cleanly
        self.timer.stop()
        self.timer = None
        # clipboard persistence. see http://www.mail-archive.com/[email protected]/msg17328.html
        event = QtCore.QEvent(QtCore.QEvent.Clipboard)
        self.app.sendEvent(self.app.clipboard(), event)
        if self.tray:
            self.tray.hide()
            self.tray.deleteLater()
            self.tray = None

    def _maybe_quit_if_no_windows_open(self) -> None:
        """Check if there are any open windows and decide whether we should quit."""
        # keep daemon running after close
        if self.config.get('daemon'):
            return
        # check if a wizard is in progress
        with self._num_wizards_lock:
            if self._num_wizards_in_progress > 0 or len(self.windows) > 0:
                return
        self.app.quit()

    def new_window(self, path, uri=None):
        # Use a signal as can be called from daemon thread
        self.app.new_window_signal.emit(path, uri)

    def show_network_dialog(self):
        if self.network_dialog:
            self.network_dialog.on_update()
            self.network_dialog.show()
            self.network_dialog.raise_()
            return
        self.network_dialog = NetworkDialog(
            network=self.daemon.network,
            config=self.config,
            network_updated_signal_obj=self.network_updated_signal_obj)
        self.network_dialog.show()

    def show_dash_net_dialog(self):
        if self.dash_net_dialog:
            self.dash_net_dialog.on_updated()
            self.dash_net_dialog.show()
            self.dash_net_dialog.raise_()
            return
        self.dash_net_dialog = DashNetDialog(network=self.daemon.network,
                                             config=self.config,
                                             dash_net_sobj=self.dash_net_sobj)
        self.dash_net_dialog.show()

    def _create_window_for_wallet(self, wallet):
        w = ElectrumWindow(self, wallet)
        self.windows.append(w)
        self.build_tray_menu()
        w.warn_if_testnet()
        w.warn_if_watching_only()
        return w

    def count_wizards_in_progress(func):
        def wrapper(self: 'ElectrumGui', *args, **kwargs):
            with self._num_wizards_lock:
                self._num_wizards_in_progress += 1
            try:
                return func(self, *args, **kwargs)
            finally:
                with self._num_wizards_lock:
                    self._num_wizards_in_progress -= 1
                self._maybe_quit_if_no_windows_open()

        return wrapper

    @count_wizards_in_progress
    def start_new_window(self,
                         path,
                         uri,
                         *,
                         app_is_starting=False) -> Optional[ElectrumWindow]:
        '''Raises the window for the wallet if it is open.  Otherwise
        opens the wallet and creates a new window for it'''
        wallet = None
        if self.config.get('tor_auto_on', True):
            network = self.daemon.network
            if network:
                proxy_modifiable = self.config.is_modifiable('proxy')
                if not proxy_modifiable or not network.detect_tor_proxy():
                    warn_d = TorWarnDialog(self, path)
                    warn_d.exec_()
                    if warn_d.result() < 0:
                        return
        try:
            wallet = self.daemon.load_wallet(path, None)
        except Exception as e:
            self.logger.exception('')
            custom_message_box(icon=QMessageBox.Warning,
                               parent=None,
                               title=_('Error'),
                               text=_('Cannot load wallet') + ' (1):\n' +
                               repr(e))
            # if app is starting, still let wizard to appear
            if not app_is_starting:
                return
        if not wallet:
            try:
                wallet = self._start_wizard_to_select_or_create_wallet(path)
            except (WalletFileException, BitcoinException) as e:
                self.logger.exception('')
                custom_message_box(icon=QMessageBox.Warning,
                                   parent=None,
                                   title=_('Error'),
                                   text=_('Cannot load wallet') + ' (2):\n' +
                                   repr(e))
        if not wallet:
            return
        # create or raise window
        try:
            for window in self.windows:
                if window.wallet.storage.path == wallet.storage.path:
                    break
            else:
                window = self._create_window_for_wallet(wallet)
        except Exception as e:
            self.logger.exception('')
            custom_message_box(icon=QMessageBox.Warning,
                               parent=None,
                               title=_('Error'),
                               text=_('Cannot create window for wallet') +
                               ':\n' + repr(e))
            if app_is_starting:
                wallet_dir = os.path.dirname(path)
                path = os.path.join(wallet_dir,
                                    get_new_wallet_name(wallet_dir))
                self.start_new_window(path, uri)
            return
        if uri:
            window.pay_to_URI(uri)
        window.bring_to_top()
        window.setWindowState(window.windowState() & ~QtCore.Qt.WindowMinimized
                              | QtCore.Qt.WindowActive)

        window.activateWindow()
        return window

    def _start_wizard_to_select_or_create_wallet(
            self, path) -> Optional[Abstract_Wallet]:
        wizard = InstallWizard(self.config,
                               self.app,
                               self.plugins,
                               gui_object=self)
        try:
            path, storage = wizard.select_storage(path, self.daemon.get_wallet)
            # storage is None if file does not exist
            if storage is None:
                wizard.path = path  # needed by trustedcoin plugin
                wizard.run('new')
                storage, db = wizard.create_storage(path)
                if db.check_unfinished_multisig():
                    wizard.show_message(_('Saved unfinished multisig wallet'))
                    return
            else:
                db = WalletDB(storage.read(), manual_upgrades=False)
                if db.upgrade_done:
                    storage.backup_old_version()
                wizard.run_upgrades(storage, db)
            if getattr(storage, 'backup_message', None):
                custom_message_box(icon=QMessageBox.Warning,
                                   parent=None,
                                   title=_('Information'),
                                   text=storage.backup_message)
                storage.backup_message = ''
            if db.check_unfinished_multisig():
                wizard.continue_multisig_setup(storage)
                storage, db = wizard.create_storage(storage.path)
                if db.check_unfinished_multisig():
                    wizard.show_message(_('Saved unfinished multisig wallet'))
                    return
        except (UserCancelled, GoBack):
            return
        except WalletAlreadyOpenInMemory as e:
            return e.wallet
        finally:
            wizard.terminate()
        # return if wallet creation is not complete
        if storage is None or db.get_action():
            return
        wallet = Wallet(db, storage, config=self.config)
        wallet.start_network(self.daemon.network)
        self.daemon.add_wallet(wallet)
        return wallet

    def close_window(self, window: ElectrumWindow):
        if window in self.windows:
            self.windows.remove(window)
        self.build_tray_menu()
        # save wallet path of last open window
        if not self.windows:
            self.config.save_last_wallet(window.wallet)
        run_hook('on_close_window', window)
        self.daemon.stop_wallet(window.wallet.storage.path)

    def init_network(self):
        # Show network dialog if config does not exist
        if self.daemon.network:
            if self.config.get('auto_connect') is None:
                wizard = InstallWizard(self.config,
                                       self.app,
                                       self.plugins,
                                       gui_object=self)
                wizard.init_network(self.daemon.network)
                wizard.terminate()

    def main(self):
        # setup Ctrl-C handling and tear-down code first, so that user can easily exit whenever
        self.app.setQuitOnLastWindowClosed(
            False)  # so _we_ can decide whether to quit
        self.app.lastWindowClosed.connect(self._maybe_quit_if_no_windows_open)
        self.app.aboutToQuit.connect(self._cleanup_before_exit)
        signal.signal(signal.SIGINT, lambda *args: self.app.quit())
        # hook for crash reporter
        Exception_Hook.maybe_setup(config=self.config)
        # first-start network-setup
        try:
            self.init_network()
        except UserCancelled:
            return
        except GoBack:
            return
        except Exception as e:
            self.logger.exception('')
            return
        # start wizard to select/create wallet
        self.timer.start()
        path = self.config.get_wallet_path(use_gui_last_wallet=True)
        try:
            if not self.start_new_window(
                    path, self.config.get('url'), app_is_starting=True):
                return
        except Exception as e:
            self.logger.error(
                "error loading wallet (or creating window for it)")
            send_exception_to_crash_reporter(e)
            # Let Qt event loop start properly so that crash reporter window can appear.
            # We will shutdown when the user closes that window, via lastWindowClosed signal.
        # main loop
        self.logger.info("starting Qt main loop")
        self.app.exec_()
        # on some platforms the exec_ call may not return, so use _cleanup_before_exit

    def stop(self):
        self.logger.info('closing GUI')
        self.app.quit()
Beispiel #2
0
class TriblerWindow(QMainWindow):
    resize_event = pyqtSignal()
    escape_pressed = pyqtSignal()
    received_search_completions = pyqtSignal(object)

    def on_exception(self, *exc_info):
        if self.exception_handler_called:
            # We only show one feedback dialog, even when there are two consecutive exceptions.
            return

        self.exception_handler_called = True

        if self.tray_icon:
            try:
                self.tray_icon.deleteLater()
            except RuntimeError:
                # The tray icon might have already been removed when unloading Qt.
                # This is due to the C code actually being asynchronous.
                logging.debug("Tray icon already removed, no further deletion necessary.")
            self.tray_icon = None

        # Stop the download loop
        self.downloads_page.stop_loading_downloads()

        # Add info about whether we are stopping Tribler or not
        os.environ['TRIBLER_SHUTTING_DOWN'] = str(self.core_manager.shutting_down)

        if not self.core_manager.shutting_down:
            self.core_manager.stop(stop_app_on_shutdown=False)

        self.setHidden(True)

        if self.debug_window:
            self.debug_window.setHidden(True)

        exception_text = "".join(traceback.format_exception(*exc_info))
        logging.error(exception_text)

        dialog = FeedbackDialog(self, exception_text, self.core_manager.events_manager.tribler_version,
                                self.start_time)
        dialog.show()

    def __init__(self, core_args=None, core_env=None, api_port=None):
        QMainWindow.__init__(self)

        QCoreApplication.setOrganizationDomain("nl")
        QCoreApplication.setOrganizationName("TUDelft")
        QCoreApplication.setApplicationName("Tribler")
        QCoreApplication.setAttribute(Qt.AA_UseHighDpiPixmaps)

        self.gui_settings = QSettings()
        api_port = api_port or int(get_gui_setting(self.gui_settings, "api_port", DEFAULT_API_PORT))
        dispatcher.update_worker_settings(port=api_port)

        self.navigation_stack = []
        self.tribler_started = False
        self.tribler_settings = None
        self.debug_window = None
        self.core_manager = CoreManager(api_port)
        self.pending_requests = {}
        self.pending_uri_requests = []
        self.download_uri = None
        self.dialog = None
        self.new_version_dialog = None
        self.start_download_dialog_active = False
        self.request_mgr = None
        self.search_request_mgr = None
        self.search_suggestion_mgr = None
        self.selected_torrent_files = []
        self.vlc_available = True
        self.has_search_results = False
        self.last_search_query = None
        self.last_search_time = None
        self.start_time = time.time()
        self.exception_handler_called = False
        self.token_refresh_timer = None

        sys.excepthook = self.on_exception

        uic.loadUi(get_ui_file_path('mainwindow.ui'), self)
        TriblerRequestManager.window = self
        self.tribler_status_bar.hide()

        # Load dynamic widgets
        uic.loadUi(get_ui_file_path('torrent_channel_list_container.ui'), self.channel_page_container)
        self.channel_torrents_list = self.channel_page_container.items_list
        self.channel_torrents_detail_widget = self.channel_page_container.details_tab_widget
        self.channel_torrents_detail_widget.initialize_details_widget()
        self.channel_torrents_list.itemSelectionChanged.connect(self.channel_page.clicked_item)

        uic.loadUi(get_ui_file_path('torrent_channel_list_container.ui'), self.search_page_container)
        self.search_results_list = self.search_page_container.items_list
        self.search_torrents_detail_widget = self.search_page_container.details_tab_widget
        self.search_torrents_detail_widget.initialize_details_widget()
        self.search_results_list.itemClicked.connect(self.on_channel_item_click)
        self.search_results_list.itemSelectionChanged.connect(self.search_results_page.clicked_item)
        self.token_balance_widget.mouseReleaseEvent = self.on_token_balance_click

        def on_state_update(new_state):
            self.loading_text_label.setText(new_state)

        self.core_manager.core_state_update.connect(on_state_update)

        self.magnet_handler = MagnetHandler(self.window)
        QDesktopServices.setUrlHandler("magnet", self.magnet_handler, "on_open_magnet_link")

        self.debug_pane_shortcut = QShortcut(QKeySequence("Ctrl+d"), self)
        self.debug_pane_shortcut.activated.connect(self.clicked_menu_button_debug)

        # Remove the focus rect on OS X
        for widget in self.findChildren(QLineEdit) + self.findChildren(QListWidget) + self.findChildren(QTreeWidget):
            widget.setAttribute(Qt.WA_MacShowFocusRect, 0)

        self.menu_buttons = [self.left_menu_button_home, self.left_menu_button_search, self.left_menu_button_my_channel,
                             self.left_menu_button_subscriptions, self.left_menu_button_video_player,
                             self.left_menu_button_downloads, self.left_menu_button_discovered]

        self.video_player_page.initialize_player()
        self.search_results_page.initialize_search_results_page()
        self.settings_page.initialize_settings_page()
        self.subscribed_channels_page.initialize()
        self.edit_channel_page.initialize_edit_channel_page()
        self.downloads_page.initialize_downloads_page()
        self.home_page.initialize_home_page()
        self.loading_page.initialize_loading_page()
        self.discovering_page.initialize_discovering_page()
        self.discovered_page.initialize_discovered_page()
        self.trust_page.initialize_trust_page()

        self.stackedWidget.setCurrentIndex(PAGE_LOADING)

        # Create the system tray icon
        if QSystemTrayIcon.isSystemTrayAvailable():
            self.tray_icon = QSystemTrayIcon()
            use_monochrome_icon = get_gui_setting(self.gui_settings, "use_monochrome_icon", False, is_bool=True)
            self.update_tray_icon(use_monochrome_icon)

            # Create the tray icon menu
            menu = self.create_add_torrent_menu()
            show_downloads_action = QAction('Show downloads', self)
            show_downloads_action.triggered.connect(self.clicked_menu_button_downloads)
            token_balance_action = QAction('Show token balance', self)
            token_balance_action.triggered.connect(lambda: self.on_token_balance_click(None))
            quit_action = QAction('Quit Tribler', self)
            quit_action.triggered.connect(self.close_tribler)
            menu.addSeparator()
            menu.addAction(show_downloads_action)
            menu.addAction(token_balance_action)
            menu.addSeparator()
            menu.addAction(quit_action)
            self.tray_icon.setContextMenu(menu)
        else:
            self.tray_icon = None

        self.hide_left_menu_playlist()
        self.left_menu_button_debug.setHidden(True)
        self.top_menu_button.setHidden(True)
        self.left_menu.setHidden(True)
        self.token_balance_widget.setHidden(True)
        self.settings_button.setHidden(True)
        self.add_torrent_button.setHidden(True)
        self.top_search_bar.setHidden(True)

        # Set various icons
        self.top_menu_button.setIcon(QIcon(get_image_path('menu.png')))

        self.search_completion_model = QStringListModel()
        completer = QCompleter()
        completer.setModel(self.search_completion_model)
        completer.setCompletionMode(QCompleter.UnfilteredPopupCompletion)
        self.item_delegate = QStyledItemDelegate()
        completer.popup().setItemDelegate(self.item_delegate)
        completer.popup().setStyleSheet("""
        QListView {
            background-color: #404040;
        }

        QListView::item {
            color: #D0D0D0;
            padding-top: 5px;
            padding-bottom: 5px;
        }

        QListView::item:hover {
            background-color: #707070;
        }
        """)
        self.top_search_bar.setCompleter(completer)

        # Toggle debug if developer mode is enabled
        self.window().left_menu_button_debug.setHidden(
            not get_gui_setting(self.gui_settings, "debug", False, is_bool=True))

        # Start Tribler
        self.core_manager.start(core_args=core_args, core_env=core_env)

        self.core_manager.events_manager.received_search_result_channel.connect(
            self.search_results_page.received_search_result_channel)
        self.core_manager.events_manager.received_search_result_torrent.connect(
            self.search_results_page.received_search_result_torrent)
        self.core_manager.events_manager.torrent_finished.connect(self.on_torrent_finished)
        self.core_manager.events_manager.new_version_available.connect(self.on_new_version_available)
        self.core_manager.events_manager.tribler_started.connect(self.on_tribler_started)
        self.core_manager.events_manager.events_started.connect(self.on_events_started)
        self.core_manager.events_manager.low_storage_signal.connect(self.on_low_storage)

        # Install signal handler for ctrl+c events
        def sigint_handler(*_):
            self.close_tribler()

        signal.signal(signal.SIGINT, sigint_handler)

        self.installEventFilter(self.video_player_page)

        # Resize the window according to the settings
        center = QApplication.desktop().availableGeometry(self).center()
        pos = self.gui_settings.value("pos", QPoint(center.x() - self.width() * 0.5, center.y() - self.height() * 0.5))
        size = self.gui_settings.value("size", self.size())

        self.move(pos)
        self.resize(size)

        self.show()

    def update_tray_icon(self, use_monochrome_icon):
        if not QSystemTrayIcon.isSystemTrayAvailable():
            return

        if use_monochrome_icon:
            self.tray_icon.setIcon(QIcon(QPixmap(get_image_path('monochrome_tribler.png'))))
        else:
            self.tray_icon.setIcon(QIcon(QPixmap(get_image_path('tribler.png'))))
        self.tray_icon.show()

    def on_low_storage(self):
        """
        Dealing with low storage space available. First stop the downloads and the core manager and ask user to user to
        make free space.
        :return:
        """
        self.downloads_page.stop_loading_downloads()
        self.core_manager.stop(False)
        close_dialog = ConfirmationDialog(self.window(), "<b>CRITICAL ERROR</b>",
                                          "You are running low on disk space (<100MB). Please make sure to have "
                                          "sufficient free space available and restart Tribler again.",
                                          [("Close Tribler", BUTTON_TYPE_NORMAL)])
        close_dialog.button_clicked.connect(lambda _: self.close_tribler())
        close_dialog.show()

    def on_torrent_finished(self, torrent_info):
        if self.tray_icon:
            self.window().tray_icon.showMessage("Download finished",
                                                "Download of %s has finished." % torrent_info["name"])

    def show_loading_screen(self):
        self.top_menu_button.setHidden(True)
        self.left_menu.setHidden(True)
        self.token_balance_widget.setHidden(True)
        self.settings_button.setHidden(True)
        self.add_torrent_button.setHidden(True)
        self.top_search_bar.setHidden(True)
        self.stackedWidget.setCurrentIndex(PAGE_LOADING)

    def on_tribler_started(self):
        self.tribler_started = True

        self.top_menu_button.setHidden(False)
        self.left_menu.setHidden(False)
        self.token_balance_widget.setHidden(False)
        self.settings_button.setHidden(False)
        self.add_torrent_button.setHidden(False)
        self.top_search_bar.setHidden(False)

        # fetch the settings, needed for the video player port
        self.request_mgr = TriblerRequestManager()
        self.fetch_settings()

        self.downloads_page.start_loading_downloads()
        self.home_page.load_popular_torrents()
        if not self.gui_settings.value("first_discover", False) and not self.core_manager.use_existing_core:
            self.window().gui_settings.setValue("first_discover", True)
            self.discovering_page.is_discovering = True
            self.stackedWidget.setCurrentIndex(PAGE_DISCOVERING)
        else:
            self.clicked_menu_button_home()

        self.setAcceptDrops(True)

    def on_events_started(self, json_dict):
        self.setWindowTitle("Tribler %s" % json_dict["version"])

    def show_status_bar(self, message):
        self.tribler_status_bar_label.setText(message)
        self.tribler_status_bar.show()

    def hide_status_bar(self):
        self.tribler_status_bar.hide()

    def process_uri_request(self):
        """
        Process a URI request if we have one in the queue.
        """
        if len(self.pending_uri_requests) == 0:
            return

        uri = self.pending_uri_requests.pop()
        if uri.startswith('file') or uri.startswith('magnet'):
            self.start_download_from_uri(uri)

    def perform_start_download_request(self, uri, anon_download, safe_seeding, destination, selected_files,
                                       total_files=0, callback=None):
        # Check if destination directory is writable
        if not is_dir_writable(destination):
            ConfirmationDialog.show_message(self.window(), "Download error <i>%s</i>" % uri,
                                            "Insufficient write permissions to <i>%s</i> directory. "
                                            "Please add proper write permissions on the directory and "
                                            "add the torrent again." % destination, "OK")
            return

        selected_files_uri = ""
        if len(selected_files) != total_files:  # Not all files included
            selected_files_uri = u'&' + u''.join(u"selected_files[]=%s&" %
                                                 quote_plus_unicode(filename) for filename in selected_files)[:-1]

        anon_hops = int(self.tribler_settings['download_defaults']['number_hops']) if anon_download else 0
        safe_seeding = 1 if safe_seeding else 0
        post_data = "uri=%s&anon_hops=%d&safe_seeding=%d&destination=%s%s" % (quote_plus_unicode(uri), anon_hops,
                                                                              safe_seeding, destination,
                                                                              selected_files_uri)
        post_data = post_data.encode('utf-8')  # We need to send bytes in the request, not unicode

        request_mgr = TriblerRequestManager()
        request_mgr.perform_request("downloads", callback if callback else self.on_download_added,
                                    method='PUT', data=post_data)

        # Save the download location to the GUI settings
        current_settings = get_gui_setting(self.gui_settings, "recent_download_locations", "")
        recent_locations = current_settings.split(",") if len(current_settings) > 0 else []
        if isinstance(destination, unicode):
            destination = destination.encode('utf-8')
        encoded_destination = destination.encode('hex')
        if encoded_destination in recent_locations:
            recent_locations.remove(encoded_destination)
        recent_locations.insert(0, encoded_destination)

        if len(recent_locations) > 5:
            recent_locations = recent_locations[:5]

        self.gui_settings.setValue("recent_download_locations", ','.join(recent_locations))

    def on_new_version_available(self, version):
        if version == str(self.gui_settings.value('last_reported_version')):
            return

        self.new_version_dialog = ConfirmationDialog(self, "New version available",
                                                     "Version %s of Tribler is available.Do you want to visit the "
                                                     "website to download the newest version?" % version,
                                                     [('IGNORE', BUTTON_TYPE_NORMAL), ('LATER', BUTTON_TYPE_NORMAL),
                                                      ('OK', BUTTON_TYPE_NORMAL)])
        self.new_version_dialog.button_clicked.connect(lambda action: self.on_new_version_dialog_done(version, action))
        self.new_version_dialog.show()

    def on_new_version_dialog_done(self, version, action):
        if action == 0:  # ignore
            self.gui_settings.setValue("last_reported_version", version)
        elif action == 2:  # ok
            import webbrowser
            webbrowser.open("https://tribler.org")

        self.new_version_dialog.close_dialog()
        self.new_version_dialog = None

    def on_search_text_change(self, text):
        self.search_suggestion_mgr = TriblerRequestManager()
        self.search_suggestion_mgr.perform_request(
            "search/completions?q=%s" % text, self.on_received_search_completions)

    def on_received_search_completions(self, completions):
        if completions is None:
            return
        self.received_search_completions.emit(completions)
        self.search_completion_model.setStringList(completions["completions"])

    def fetch_settings(self):
        self.request_mgr = TriblerRequestManager()
        self.request_mgr.perform_request("settings", self.received_settings, capture_errors=False)

    def received_settings(self, settings):
        if not settings:
            return
        # If we cannot receive the settings, stop Tribler with an option to send the crash report.
        if 'error' in settings:
            raise RuntimeError(TriblerRequestManager.get_message_from_error(settings))

        self.tribler_settings = settings['settings']

        # Set the video server port
        self.video_player_page.video_player_port = settings["ports"]["video_server~port"]

        # Disable various components based on the settings
        if not self.tribler_settings['search_community']['enabled']:
            self.window().top_search_bar.setHidden(True)
        if not self.tribler_settings['video_server']['enabled']:
            self.left_menu_button_video_player.setHidden(True)
        self.downloads_creditmining_button.setHidden(not self.tribler_settings["credit_mining"]["enabled"])
        self.downloads_all_button.click()

        # process pending file requests (i.e. someone clicked a torrent file when Tribler was closed)
        # We do this after receiving the settings so we have the default download location.
        self.process_uri_request()

        # Set token balance refresh timer and load the token balance
        self.token_refresh_timer = QTimer()
        self.token_refresh_timer.timeout.connect(self.load_token_balance)
        self.token_refresh_timer.start(60000)

        self.load_token_balance()

    def on_top_search_button_click(self):
        current_ts = time.time()
        current_search_query = self.top_search_bar.text()

        if self.last_search_query and self.last_search_time \
                and self.last_search_query == self.top_search_bar.text() \
                and current_ts - self.last_search_time < 1:
            logging.info("Same search query already sent within 500ms so dropping this one")
            return

        self.left_menu_button_search.setChecked(True)
        self.has_search_results = True
        self.clicked_menu_button_search()
        self.search_results_page.perform_search(current_search_query)
        self.search_request_mgr = TriblerRequestManager()
        self.search_request_mgr.perform_request("search?q=%s" % current_search_query, None)
        self.last_search_query = current_search_query
        self.last_search_time = current_ts

    def on_settings_button_click(self):
        self.deselect_all_menu_buttons()
        self.stackedWidget.setCurrentIndex(PAGE_SETTINGS)
        self.settings_page.load_settings()
        self.navigation_stack = []
        self.hide_left_menu_playlist()

    def on_token_balance_click(self, _):
        self.raise_window()
        self.deselect_all_menu_buttons()
        self.stackedWidget.setCurrentIndex(PAGE_TRUST)
        self.trust_page.load_trust_statistics()
        self.navigation_stack = []
        self.hide_left_menu_playlist()

    def load_token_balance(self):
        self.request_mgr = TriblerRequestManager()
        self.request_mgr.perform_request("trustchain/statistics", self.received_token_balance, capture_errors=False)

    def received_token_balance(self, statistics):
        if not statistics or "statistics" not in statistics:
            return

        statistics = statistics["statistics"]
        if 'latest_block' in statistics:
            balance = (statistics["latest_block"]["transaction"]["total_up"] -
                       statistics["latest_block"]["transaction"]["total_down"])
            self.set_token_balance(balance)
        else:
            self.token_balance_label.setText("0 MB")

    def set_token_balance(self, balance):
        if abs(balance) > 1024 ** 4:    # Balance is over a TB
            balance /= 1024.0 ** 4
            self.token_balance_label.setText("%.1f TB" % balance)
        elif abs(balance) > 1024 ** 3:  # Balance is over a GB
            balance /= 1024.0 ** 3
            self.token_balance_label.setText("%.1f GB" % balance)
        else:
            balance /= 1024.0 ** 2
            self.token_balance_label.setText("%d MB" % balance)

    def raise_window(self):
        self.setWindowState(self.windowState() & ~Qt.WindowMinimized | Qt.WindowActive)
        self.raise_()
        self.activateWindow()

    def create_add_torrent_menu(self):
        """
        Create a menu to add new torrents. Shows when users click on the tray icon or the big plus button.
        """
        menu = TriblerActionMenu(self)

        browse_files_action = QAction('Import torrent from file', self)
        browse_directory_action = QAction('Import torrent(s) from directory', self)
        add_url_action = QAction('Import torrent from magnet/URL', self)

        browse_files_action.triggered.connect(self.on_add_torrent_browse_file)
        browse_directory_action.triggered.connect(self.on_add_torrent_browse_dir)
        add_url_action.triggered.connect(self.on_add_torrent_from_url)

        menu.addAction(browse_files_action)
        menu.addAction(browse_directory_action)
        menu.addAction(add_url_action)

        return menu

    def on_add_torrent_button_click(self, pos):
        self.create_add_torrent_menu().exec_(self.mapToGlobal(self.add_torrent_button.pos()))

    def on_add_torrent_browse_file(self):
        filenames = QFileDialog.getOpenFileNames(self,
                                                 "Please select the .torrent file",
                                                 QDir.homePath(),
                                                 "Torrent files (*.torrent)")
        if len(filenames[0]) > 0:
            [self.pending_uri_requests.append(u"file:%s" % filename) for filename in filenames[0]]
            self.process_uri_request()

    def start_download_from_uri(self, uri):
        self.download_uri = uri

        if get_gui_setting(self.gui_settings, "ask_download_settings", True, is_bool=True):
            # If tribler settings is not available, fetch the settings and inform the user to try again.
            if not self.tribler_settings:
                self.fetch_settings()
                ConfirmationDialog.show_error(self, "Download Error", "Tribler settings is not available yet. "
                                                                      "Fetching it now. Please try again later.")
                return
            # Clear any previous dialog if exists
            if self.dialog:
                self.dialog.close_dialog()
                self.dialog = None

            self.dialog = StartDownloadDialog(self, self.download_uri)
            self.dialog.button_clicked.connect(self.on_start_download_action)
            self.dialog.show()
            self.start_download_dialog_active = True
        else:
            # In the unlikely scenario that tribler settings are not available yet, try to fetch settings again and
            # add the download uri back to self.pending_uri_requests to process again.
            if not self.tribler_settings:
                self.fetch_settings()
                if self.download_uri not in self.pending_uri_requests:
                    self.pending_uri_requests.append(self.download_uri)
                return

            self.window().perform_start_download_request(self.download_uri,
                                                         self.window().tribler_settings['download_defaults'][
                                                             'anonymity_enabled'],
                                                         self.window().tribler_settings['download_defaults'][
                                                             'safeseeding_enabled'],
                                                         self.tribler_settings['download_defaults']['saveas'], [], 0)
            self.process_uri_request()

    def on_start_download_action(self, action):
        if action == 1:
            if self.dialog and self.dialog.dialog_widget:
                self.window().perform_start_download_request(
                    self.download_uri, self.dialog.dialog_widget.anon_download_checkbox.isChecked(),
                    self.dialog.dialog_widget.safe_seed_checkbox.isChecked(),
                    self.dialog.dialog_widget.destination_input.currentText(),
                    self.dialog.get_selected_files(),
                    self.dialog.dialog_widget.files_list_view.topLevelItemCount())
            else:
                ConfirmationDialog.show_error(self, "Tribler UI Error", "Something went wrong. Please try again.")
                logging.exception("Error while trying to download. Either dialog or dialog.dialog_widget is None")

        self.dialog.request_mgr.cancel_request()  # To abort the torrent info request
        self.dialog.close_dialog()
        self.dialog = None
        self.start_download_dialog_active = False

        if action == 0:  # We do this after removing the dialog since process_uri_request is blocking
            self.process_uri_request()

    def on_add_torrent_browse_dir(self):
        chosen_dir = QFileDialog.getExistingDirectory(self,
                                                      "Please select the directory containing the .torrent files",
                                                      QDir.homePath(),
                                                      QFileDialog.ShowDirsOnly)

        if len(chosen_dir) != 0:
            self.selected_torrent_files = [torrent_file for torrent_file in glob.glob(chosen_dir + "/*.torrent")]
            self.dialog = ConfirmationDialog(self, "Add torrents from directory",
                                             "Are you sure you want to add %d torrents to Tribler?" %
                                             len(self.selected_torrent_files),
                                             [('ADD', BUTTON_TYPE_NORMAL), ('CANCEL', BUTTON_TYPE_CONFIRM)])
            self.dialog.button_clicked.connect(self.on_confirm_add_directory_dialog)
            self.dialog.show()

    def on_confirm_add_directory_dialog(self, action):
        if action == 0:
            for torrent_file in self.selected_torrent_files:
                escaped_uri = u"file:%s" % pathname2url(torrent_file)
                self.perform_start_download_request(escaped_uri,
                                                    self.window().tribler_settings['download_defaults'][
                                                         'anonymity_enabled'],
                                                    self.window().tribler_settings['download_defaults'][
                                                         'safeseeding_enabled'],
                                                    self.tribler_settings['download_defaults']['saveas'], [], 0)

        if self.dialog:
            self.dialog.close_dialog()
            self.dialog = None

    def on_add_torrent_from_url(self):
        # Make sure that the window is visible (this action might be triggered from the tray icon)
        self.raise_window()

        if self.video_player_page.isVisible():
            # If we're adding a torrent from the video player page, go to the home page.
            # This is necessary since VLC takes the screen and the popup becomes invisible.
            self.clicked_menu_button_home()

        self.dialog = ConfirmationDialog(self, "Add torrent from URL/magnet link",
                                         "Please enter the URL/magnet link in the field below:",
                                         [('ADD', BUTTON_TYPE_NORMAL), ('CANCEL', BUTTON_TYPE_CONFIRM)],
                                         show_input=True)
        self.dialog.dialog_widget.dialog_input.setPlaceholderText('URL/magnet link')
        self.dialog.dialog_widget.dialog_input.setFocus()
        self.dialog.button_clicked.connect(self.on_torrent_from_url_dialog_done)
        self.dialog.show()

    def on_torrent_from_url_dialog_done(self, action):
        if self.dialog and self.dialog.dialog_widget:
            uri = self.dialog.dialog_widget.dialog_input.text()

            # Remove first dialog
            self.dialog.close_dialog()
            self.dialog = None

            if action == 0:
                self.start_download_from_uri(uri)

    def on_download_added(self, result):
        if not result:
            return
        if len(self.pending_uri_requests) == 0:  # Otherwise, we first process the remaining requests.
            self.window().left_menu_button_downloads.click()
        else:
            self.process_uri_request()

    def on_top_menu_button_click(self):
        if self.left_menu.isHidden():
            self.left_menu.show()
        else:
            self.left_menu.hide()

    def deselect_all_menu_buttons(self, except_select=None):
        for button in self.menu_buttons:
            if button == except_select:
                button.setEnabled(False)
                continue
            button.setEnabled(True)

            if button == self.left_menu_button_search and not self.has_search_results:
                button.setEnabled(False)

            button.setChecked(False)

    def clicked_menu_button_home(self):
        self.deselect_all_menu_buttons(self.left_menu_button_home)
        self.stackedWidget.setCurrentIndex(PAGE_HOME)
        self.navigation_stack = []
        self.hide_left_menu_playlist()

    def clicked_menu_button_search(self):
        self.deselect_all_menu_buttons(self.left_menu_button_search)
        self.stackedWidget.setCurrentIndex(PAGE_SEARCH_RESULTS)
        self.navigation_stack = []
        self.hide_left_menu_playlist()

    def clicked_menu_button_discovered(self):
        self.deselect_all_menu_buttons(self.left_menu_button_discovered)
        self.stackedWidget.setCurrentIndex(PAGE_DISCOVERED)
        self.discovered_page.load_discovered_channels()
        self.navigation_stack = []
        self.hide_left_menu_playlist()

    def clicked_menu_button_my_channel(self):
        self.deselect_all_menu_buttons(self.left_menu_button_my_channel)
        self.stackedWidget.setCurrentIndex(PAGE_EDIT_CHANNEL)
        self.edit_channel_page.load_my_channel_overview()
        self.navigation_stack = []
        self.hide_left_menu_playlist()

    def clicked_menu_button_video_player(self):
        self.deselect_all_menu_buttons(self.left_menu_button_video_player)
        self.stackedWidget.setCurrentIndex(PAGE_VIDEO_PLAYER)
        self.navigation_stack = []
        self.show_left_menu_playlist()

    def clicked_menu_button_downloads(self):
        self.raise_window()
        self.left_menu_button_downloads.setChecked(True)
        self.deselect_all_menu_buttons(self.left_menu_button_downloads)
        self.stackedWidget.setCurrentIndex(PAGE_DOWNLOADS)
        self.navigation_stack = []
        self.hide_left_menu_playlist()

    def clicked_menu_button_debug(self):
        if not self.debug_window:
            self.debug_window = DebugWindow(self.tribler_settings, self.core_manager.events_manager.tribler_version)
        self.debug_window.show()

    def clicked_menu_button_subscriptions(self):
        self.deselect_all_menu_buttons(self.left_menu_button_subscriptions)
        self.subscribed_channels_page.load_subscribed_channels()
        self.stackedWidget.setCurrentIndex(PAGE_SUBSCRIBED_CHANNELS)
        self.navigation_stack = []
        self.hide_left_menu_playlist()

    def hide_left_menu_playlist(self):
        self.left_menu_seperator.setHidden(True)
        self.left_menu_playlist_label.setHidden(True)
        self.left_menu_playlist.setHidden(True)

    def show_left_menu_playlist(self):
        self.left_menu_seperator.setHidden(False)
        self.left_menu_playlist_label.setHidden(False)
        self.left_menu_playlist.setHidden(False)

    def on_channel_item_click(self, channel_list_item):
        list_widget = channel_list_item.listWidget()
        from TriblerGUI.widgets.channel_list_item import ChannelListItem
        if isinstance(list_widget.itemWidget(channel_list_item), ChannelListItem):
            channel_info = channel_list_item.data(Qt.UserRole)
            self.channel_page.initialize_with_channel(channel_info)
            self.navigation_stack.append(self.stackedWidget.currentIndex())
            self.stackedWidget.setCurrentIndex(PAGE_CHANNEL_DETAILS)

    def on_playlist_item_click(self, playlist_list_item):
        list_widget = playlist_list_item.listWidget()
        from TriblerGUI.widgets.playlist_list_item import PlaylistListItem
        if isinstance(list_widget.itemWidget(playlist_list_item), PlaylistListItem):
            playlist_info = playlist_list_item.data(Qt.UserRole)
            self.playlist_page.initialize_with_playlist(playlist_info)
            self.navigation_stack.append(self.stackedWidget.currentIndex())
            self.stackedWidget.setCurrentIndex(PAGE_PLAYLIST_DETAILS)

    def on_page_back_clicked(self):
        try:
            prev_page = self.navigation_stack.pop()
            self.stackedWidget.setCurrentIndex(prev_page)
            if prev_page == PAGE_SEARCH_RESULTS:
                self.stackedWidget.widget(prev_page).load_search_results_in_list()
            if prev_page == PAGE_SUBSCRIBED_CHANNELS:
                self.stackedWidget.widget(prev_page).load_subscribed_channels()
            if prev_page == PAGE_DISCOVERED:
                self.stackedWidget.widget(prev_page).load_discovered_channels()
        except IndexError:
            logging.exception("Unknown page found in stack")

    def on_edit_channel_clicked(self):
        self.stackedWidget.setCurrentIndex(PAGE_EDIT_CHANNEL)
        self.navigation_stack = []
        self.channel_page.on_edit_channel_clicked()

    def resizeEvent(self, _):
        # Resize home page cells
        cell_width = self.home_page_table_view.width() / 3 - 3  # We have some padding to the right
        cell_height = cell_width / 2 + 60

        for i in range(0, 3):
            self.home_page_table_view.setColumnWidth(i, cell_width)
            self.home_page_table_view.setRowHeight(i, cell_height)
        self.resize_event.emit()

    def exit_full_screen(self):
        self.top_bar.show()
        self.left_menu.show()
        self.video_player_page.is_full_screen = False
        self.showNormal()

    def close_tribler(self):
        if not self.core_manager.shutting_down:
            def show_force_shutdown():
                self.loading_text_label.setText("Tribler is taking longer than expected to shut down. You can force "
                                                "Tribler to shutdown by pressing the button below. This might lead "
                                                "to data loss.")
                self.window().force_shutdown_btn.show()

            if self.tray_icon:
                self.tray_icon.deleteLater()
            self.show_loading_screen()
            self.hide_status_bar()
            self.loading_text_label.setText("Shutting down...")

            self.shutdown_timer = QTimer()
            self.shutdown_timer.timeout.connect(show_force_shutdown)
            self.shutdown_timer.start(SHUTDOWN_WAITING_PERIOD)

            self.gui_settings.setValue("pos", self.pos())
            self.gui_settings.setValue("size", self.size())

            if self.core_manager.use_existing_core:
                # Don't close the core that we are using
                QApplication.quit()

            self.core_manager.stop()
            self.core_manager.shutting_down = True
            self.downloads_page.stop_loading_downloads()
            request_queue.clear()

            # Stop the token balance timer
            if self.token_refresh_timer:
                self.token_refresh_timer.stop()

    def closeEvent(self, close_event):
        self.close_tribler()
        close_event.ignore()

    def keyReleaseEvent(self, event):
        if event.key() == Qt.Key_Escape:
            self.escape_pressed.emit()
            if self.isFullScreen():
                self.exit_full_screen()

    def dragEnterEvent(self, e):
        file_urls = [_qurl_to_path(url) for url in e.mimeData().urls()] if e.mimeData().hasUrls() else []

        if any(os.path.isfile(filename) for filename in file_urls):
            e.accept()
        else:
            e.ignore()

    def dropEvent(self, e):
        file_urls = ([(_qurl_to_path(url), url.toString()) for url in e.mimeData().urls()]
                     if e.mimeData().hasUrls() else [])

        for filename, fileurl in file_urls:
            if os.path.isfile(filename):
                self.start_download_from_uri(fileurl)

        e.accept()

    def clicked_force_shutdown(self):
        process_checker = ProcessChecker()
        if process_checker.already_running:
            core_pid = process_checker.get_pid_from_lock_file()
            os.kill(int(core_pid), 9)
        # Stop the Qt application
        QApplication.quit()
Beispiel #3
0
class QtApplication(QApplication, Application):
    pluginsLoaded = Signal()
    applicationRunning = Signal()

    def __init__(self, tray_icon_name: str = None, **kwargs) -> None:
        plugin_path = ""
        if sys.platform == "win32":
            if hasattr(sys, "frozen"):
                plugin_path = os.path.join(
                    os.path.dirname(os.path.abspath(sys.executable)), "PyQt5",
                    "plugins")
                Logger.log("i", "Adding QT5 plugin path: %s", plugin_path)
                QCoreApplication.addLibraryPath(plugin_path)
            else:
                import site
                for sitepackage_dir in site.getsitepackages():
                    QCoreApplication.addLibraryPath(
                        os.path.join(sitepackage_dir, "PyQt5", "plugins"))
        elif sys.platform == "darwin":
            plugin_path = os.path.join(self.getInstallPrefix(), "Resources",
                                       "plugins")

        if plugin_path:
            Logger.log("i", "Adding QT5 plugin path: %s", plugin_path)
            QCoreApplication.addLibraryPath(plugin_path)

        # use Qt Quick Scene Graph "basic" render loop
        os.environ["QSG_RENDER_LOOP"] = "basic"

        super().__init__(sys.argv, **kwargs)  # type: ignore

        self._qml_import_paths = []  #type: List[str]
        self._main_qml = "main.qml"  #type: str
        self._qml_engine = None  #type: Optional[QQmlApplicationEngine]
        self._main_window = None  #type: Optional[MainWindow]
        self._tray_icon_name = tray_icon_name  #type: Optional[str]
        self._tray_icon = None  #type: Optional[str]
        self._tray_icon_widget = None  #type: Optional[QSystemTrayIcon]
        self._theme = None  #type: Optional[Theme]
        self._renderer = None  #type: Optional[QtRenderer]

        self._job_queue = None  #type: Optional[JobQueue]
        self._version_upgrade_manager = None  #type: Optional[VersionUpgradeManager]

        self._is_shutting_down = False  #type: bool

        self._recent_files = []  #type: List[QUrl]

        self._configuration_error_message = None  #type: Optional[ConfigurationErrorMessage]

    def addCommandLineOptions(self) -> None:
        super().addCommandLineOptions()
        # This flag is used by QApplication. We don't process it.
        self._cli_parser.add_argument(
            "-qmljsdebugger", help="For Qt's QML debugger compatibility")

    def initialize(self) -> None:
        super().initialize()
        # Initialize the package manager to remove and install scheduled packages.
        self._package_manager = self._package_manager_class(self, parent=self)

        self._mesh_file_handler = MeshFileHandler(self)  #type: MeshFileHandler
        self._workspace_file_handler = WorkspaceFileHandler(
            self)  #type: WorkspaceFileHandler

        # Remove this and you will get Windows 95 style for all widgets if you are using Qt 5.10+
        self.setStyle("fusion")

        self.setAttribute(Qt.AA_UseDesktopOpenGL)
        major_version, minor_version, profile = OpenGLContext.detectBestOpenGLVersion(
        )

        if major_version is None or minor_version is None or profile is None:
            Logger.log(
                "e",
                "Startup failed because OpenGL version probing has failed: tried to create a 2.0 and 4.1 context. Exiting"
            )
            if not self.getIsHeadLess():
                QMessageBox.critical(
                    None, "Failed to probe OpenGL",
                    "Could not probe OpenGL. This program requires OpenGL 2.0 or higher. Please check your video card drivers."
                )
            sys.exit(1)
        else:
            opengl_version_str = OpenGLContext.versionAsText(
                major_version, minor_version, profile)
            Logger.log("d",
                       "Detected most suitable OpenGL context version: %s",
                       opengl_version_str)
        if not self.getIsHeadLess():
            OpenGLContext.setDefaultFormat(major_version,
                                           minor_version,
                                           profile=profile)

        self._qml_import_paths.append(
            os.path.join(os.path.dirname(sys.executable), "qml"))
        self._qml_import_paths.append(
            os.path.join(self.getInstallPrefix(), "Resources", "qml"))

        Logger.log("i", "Initializing job queue ...")
        self._job_queue = JobQueue()
        self._job_queue.jobFinished.connect(self._onJobFinished)

        Logger.log("i", "Initializing version upgrade manager ...")
        self._version_upgrade_manager = VersionUpgradeManager(self)

    def startSplashWindowPhase(self) -> None:
        super().startSplashWindowPhase()
        i18n_catalog = i18nCatalog("uranium")
        self.showSplashMessage(
            i18n_catalog.i18nc("@info:progress",
                               "Initializing package manager..."))
        self._package_manager.initialize()

        # Read preferences here (upgrade won't work) to get the language in use, so the splash window can be shown in
        # the correct language.
        try:
            preferences_filename = Resources.getPath(Resources.Preferences,
                                                     self._app_name + ".cfg")
            self._preferences.readFromFile(preferences_filename)
        except FileNotFoundError:
            Logger.log(
                "i",
                "Preferences file not found, ignore and use default language '%s'",
                self._default_language)

        signal.signal(signal.SIGINT, signal.SIG_DFL)
        # This is done here as a lot of plugins require a correct gl context. If you want to change the framework,
        # these checks need to be done in your <framework>Application.py class __init__().

        self._configuration_error_message = ConfigurationErrorMessage(
            self,
            i18n_catalog.i18nc("@info:status",
                               "Your configuration seems to be corrupt."),
            lifetime=0,
            title=i18n_catalog.i18nc("@info:title", "Configuration errors"))
        # Remove, install, and then loading plugins
        self.showSplashMessage(
            i18n_catalog.i18nc("@info:progress", "Loading plugins..."))
        # Remove and install the plugins that have been scheduled
        self._plugin_registry.initializeBeforePluginsAreLoaded()
        self._loadPlugins()
        self._plugin_registry.checkRequiredPlugins(self.getRequiredPlugins())
        self.pluginsLoaded.emit()

        self.showSplashMessage(
            i18n_catalog.i18nc("@info:progress", "Updating configuration..."))
        with self._container_registry.lockFile():
            VersionUpgradeManager.getInstance().upgrade()

        # Load preferences again because before we have loaded the plugins, we don't have the upgrade routine for
        # the preferences file. Now that we have, load the preferences file again so it can be upgraded and loaded.
        self.showSplashMessage(
            i18n_catalog.i18nc("@info:progress", "Loading preferences..."))
        try:
            preferences_filename = Resources.getPath(Resources.Preferences,
                                                     self._app_name + ".cfg")
            with open(preferences_filename, "r", encoding="utf-8") as f:
                serialized = f.read()
            # This performs the upgrade for Preferences
            self._preferences.deserialize(serialized)
            self._preferences.setValue("general/plugins_to_remove", "")
            self._preferences.writeToFile(preferences_filename)
        except (FileNotFoundError, UnicodeDecodeError):
            Logger.log(
                "i",
                "The preferences file cannot be found or it is corrupted, so we will use default values"
            )

        self.processEvents()
        # Force the configuration file to be written again since the list of plugins to remove maybe changed
        try:
            self._preferences_filename = Resources.getPath(
                Resources.Preferences, self._app_name + ".cfg")
            self._preferences.readFromFile(self._preferences_filename)
        except FileNotFoundError:
            Logger.log(
                "i",
                "The preferences file '%s' cannot be found, will use default values",
                self._preferences_filename)
            self._preferences_filename = Resources.getStoragePath(
                Resources.Preferences, self._app_name + ".cfg")

        # FIXME: This is done here because we now use "plugins.json" to manage plugins instead of the Preferences file,
        # but the PluginRegistry will still import data from the Preferences files if present, such as disabled plugins,
        # so we need to reset those values AFTER the Preferences file is loaded.
        self._plugin_registry.initializeAfterPluginsAreLoaded()

        # Check if we have just updated from an older version
        self._preferences.addPreference("general/last_run_version", "")
        last_run_version_str = self._preferences.getValue(
            "general/last_run_version")
        if not last_run_version_str:
            last_run_version_str = self._version
        last_run_version = Version(last_run_version_str)
        current_version = Version(self._version)
        if last_run_version < current_version:
            self._just_updated_from_old_version = True
        self._preferences.setValue("general/last_run_version",
                                   str(current_version))
        self._preferences.writeToFile(self._preferences_filename)

        # Preferences: recent files
        self._preferences.addPreference("%s/recent_files" % self._app_name, "")
        file_names = self._preferences.getValue("%s/recent_files" %
                                                self._app_name).split(";")
        for file_name in file_names:
            if not os.path.isfile(file_name):
                continue
            self._recent_files.append(QUrl.fromLocalFile(file_name))

        if not self.getIsHeadLess():
            # Initialize System tray icon and make it invisible because it is used only to show pop up messages
            self._tray_icon = None
            if self._tray_icon_name:
                self._tray_icon = QIcon(
                    Resources.getPath(Resources.Images, self._tray_icon_name))
                self._tray_icon_widget = QSystemTrayIcon(self._tray_icon)
                self._tray_icon_widget.setVisible(False)

    def initializeEngine(self) -> None:
        # TODO: Document native/qml import trickery
        self._qml_engine = QQmlApplicationEngine(self)
        self.processEvents()
        self._qml_engine.setOutputWarningsToStandardError(False)
        self._qml_engine.warnings.connect(self.__onQmlWarning)

        for path in self._qml_import_paths:
            self._qml_engine.addImportPath(path)

        if not hasattr(sys, "frozen"):
            self._qml_engine.addImportPath(
                os.path.join(os.path.dirname(__file__), "qml"))

        self._qml_engine.rootContext().setContextProperty(
            "QT_VERSION_STR", QT_VERSION_STR)
        self.processEvents()
        self._qml_engine.rootContext().setContextProperty(
            "screenScaleFactor", self._screenScaleFactor())

        self.registerObjects(self._qml_engine)

        Bindings.register()

        # Preload theme. The theme will be loaded on first use, which will incur a ~0.1s freeze on the MainThread.
        # Do it here, while the splash screen is shown. Also makes this freeze explicit and traceable.
        self.getTheme()
        self.processEvents()

        self.showSplashMessage(
            self._i18n_catalog.i18nc("@info:progress", "Loading UI..."))
        self._qml_engine.load(self._main_qml)
        self.engineCreatedSignal.emit()

    recentFilesChanged = pyqtSignal()

    @pyqtProperty("QVariantList", notify=recentFilesChanged)
    def recentFiles(self) -> List[QUrl]:
        return self._recent_files

    def _onJobFinished(self, job: Job) -> None:
        if isinstance(job, WriteFileJob):
            if not job.getResult() or not job.getAddToRecentFiles():
                # For a write file job, if it failed or it doesn't need to be added to the recent files list, we do not
                # add it.
                return
        elif (not isinstance(job, ReadMeshJob)
              and not isinstance(job, ReadFileJob)) or not job.getResult():
            return

        if isinstance(job, (ReadMeshJob, ReadFileJob, WriteFileJob)):
            self.addFileToRecentFiles(job.getFileName())

    def addFileToRecentFiles(self, file_name: str) -> None:
        file_path = QUrl.fromLocalFile(file_name)

        if file_path in self._recent_files:
            self._recent_files.remove(file_path)

        self._recent_files.insert(0, file_path)
        if len(self._recent_files) > 10:
            del self._recent_files[10]

        pref = ""
        for path in self._recent_files:
            pref += path.toLocalFile() + ";"

        self.getPreferences().setValue(
            "%s/recent_files" % self.getApplicationName(), pref)
        self.recentFilesChanged.emit()

    def run(self) -> None:
        super().run()

    def hideMessage(self, message: Message) -> None:
        with self._message_lock:
            if message in self._visible_messages:
                message.hide(
                    send_signal=False
                )  # we're in handling hideMessageSignal so we don't want to resend it
                self._visible_messages.remove(message)
                self.visibleMessageRemoved.emit(message)

    def showMessage(self, message: Message) -> None:
        with self._message_lock:
            if message not in self._visible_messages:
                self._visible_messages.append(message)
                message.setLifetimeTimer(QTimer())
                message.setInactivityTimer(QTimer())
                self.visibleMessageAdded.emit(message)

        # also show toast message when the main window is minimized
        self.showToastMessage(self._app_name, message.getText())

    def _onMainWindowStateChanged(self, window_state: int) -> None:
        if self._tray_icon and self._tray_icon_widget:
            visible = window_state == Qt.WindowMinimized
            self._tray_icon_widget.setVisible(visible)

    # Show toast message using System tray widget.
    def showToastMessage(self, title: str, message: str) -> None:
        if self.checkWindowMinimizedState() and self._tray_icon_widget:
            # NOTE: Qt 5.8 don't support custom icon for the system tray messages, but Qt 5.9 does.
            #       We should use the custom icon when we switch to Qt 5.9
            self._tray_icon_widget.showMessage(title, message)

    def setMainQml(self, path: str) -> None:
        self._main_qml = path

    def exec_(self, *args: Any, **kwargs: Any) -> None:
        self.applicationRunning.emit()
        super().exec_(*args, **kwargs)

    @pyqtSlot()
    def reloadQML(self) -> None:
        # only reload when it is a release build
        if not self.getIsDebugMode():
            return
        if self._qml_engine and self._theme:
            self._qml_engine.clearComponentCache()
            self._theme.reload()
            self._qml_engine.load(self._main_qml)
            # Hide the window. For some reason we can't close it yet. This needs to be done in the onComponentCompleted.
            for obj in self._qml_engine.rootObjects():
                if obj != self._qml_engine.rootObjects()[-1]:
                    obj.hide()

    @pyqtSlot()
    def purgeWindows(self) -> None:
        # Close all root objects except the last one.
        # Should only be called by onComponentCompleted of the mainWindow.
        if self._qml_engine:
            for obj in self._qml_engine.rootObjects():
                if obj != self._qml_engine.rootObjects()[-1]:
                    obj.close()

    @pyqtSlot("QList<QQmlError>")
    def __onQmlWarning(self, warnings: List[QQmlError]) -> None:
        for warning in warnings:
            Logger.log("w", warning.toString())

    engineCreatedSignal = Signal()

    def isShuttingDown(self) -> bool:
        return self._is_shutting_down

    def registerObjects(
        self, engine
    ) -> None:  #type: ignore #Don't type engine, because the type depends on the platform you're running on so it always gives an error somewhere.
        engine.rootContext().setContextProperty("PluginRegistry",
                                                PluginRegistry.getInstance())

    def getRenderer(self) -> QtRenderer:
        if not self._renderer:
            self._renderer = QtRenderer()

        return cast(QtRenderer, self._renderer)

    mainWindowChanged = Signal()

    def getMainWindow(self) -> Optional[MainWindow]:
        return self._main_window

    def setMainWindow(self, window: MainWindow) -> None:
        if window != self._main_window:
            if self._main_window is not None:
                self._main_window.windowStateChanged.disconnect(
                    self._onMainWindowStateChanged)

            self._main_window = window
            if self._main_window is not None:
                self._main_window.windowStateChanged.connect(
                    self._onMainWindowStateChanged)
            self.mainWindowChanged.emit()

    def setVisible(self, visible: bool) -> None:
        if self._main_window is not None:
            self._main_window.visible = visible

    @property
    def isVisible(self) -> bool:
        if self._main_window is not None:
            return self._main_window.visible  #type: ignore #MyPy doesn't realise that self._main_window cannot be None here.
        return False

    def getTheme(self) -> Optional[Theme]:
        if self._theme is None:
            if self._qml_engine is None:
                Logger.log(
                    "e",
                    "The theme cannot be accessed before the engine is initialised"
                )
                return None

            self._theme = UM.Qt.Bindings.Theme.Theme.getInstance(
                self._qml_engine)
        return self._theme

    #   Handle a function that should be called later.
    def functionEvent(self, event: QEvent) -> None:
        e = _QtFunctionEvent(event)
        QCoreApplication.postEvent(self, e)

    #   Handle Qt events
    def event(self, event: QEvent) -> bool:
        if event.type() == _QtFunctionEvent.QtFunctionEvent:
            event._function_event.call()
            return True

        return super().event(event)

    def windowClosed(self, save_data: bool = True) -> None:
        Logger.log("d", "Shutting down %s", self.getApplicationName())
        self._is_shutting_down = True

        # garbage collect tray icon so it gets properly closed before the application is closed
        self._tray_icon_widget = None

        if save_data:
            try:
                self.savePreferences()
            except Exception as e:
                Logger.log("e", "Exception while saving preferences: %s",
                           repr(e))

        try:
            self.applicationShuttingDown.emit()
        except Exception as e:
            Logger.log("e", "Exception while emitting shutdown signal: %s",
                       repr(e))

        try:
            self.getBackend().close()
        except Exception as e:
            Logger.log("e", "Exception while closing backend: %s", repr(e))

        if self._tray_icon_widget:
            self._tray_icon_widget.deleteLater()

        self.quit()

    def checkWindowMinimizedState(self) -> bool:
        if self._main_window is not None and self._main_window.windowState(
        ) == Qt.WindowMinimized:
            return True
        else:
            return False

    ##  Get the backend of the application (the program that does the heavy lifting).
    #   The backend is also a QObject, which can be used from qml.
    @pyqtSlot(result="QObject*")
    def getBackend(self) -> Backend:
        return self._backend

    ##  Property used to expose the backend
    #   It is made static as the backend is not supposed to change during runtime.
    #   This makes the connection between backend and QML more reliable than the pyqtSlot above.
    #   \returns Backend \type{Backend}
    @pyqtProperty("QVariant", constant=True)
    def backend(self) -> Backend:
        return self.getBackend()

    ## Create a class variable so we can manage the splash in the CrashHandler dialog when the Application instance
    # is not yet created, e.g. when an error occurs during the initialization
    splash = None  # type: Optional[QSplashScreen]

    def createSplash(self) -> None:
        if not self.getIsHeadLess():
            try:
                QtApplication.splash = self._createSplashScreen()
            except FileNotFoundError:
                QtApplication.splash = None
            else:
                if QtApplication.splash:
                    QtApplication.splash.show()
                    self.processEvents()

    ##  Display text on the splash screen.
    def showSplashMessage(self, message: str) -> None:
        if not QtApplication.splash:
            self.createSplash()

        if QtApplication.splash:
            self.processEvents(
            )  # Process events from previous loading phase before updating the message
            QtApplication.splash.showMessage(
                message,
                Qt.AlignHCenter | Qt.AlignVCenter)  # Now update the message
            self.processEvents()  # And make sure it is immediately visible
        elif self.getIsHeadLess():
            Logger.log("d", message)

    ##  Close the splash screen after the application has started.
    def closeSplash(self) -> None:
        if QtApplication.splash:
            QtApplication.splash.close()
            QtApplication.splash = None

    ## Create a QML component from a qml file.
    #  \param qml_file_path: The absolute file path to the root qml file.
    #  \param context_properties: Optional dictionary containing the properties that will be set on the context of the
    #                              qml instance before creation.
    #  \return None in case the creation failed (qml error), else it returns the qml instance.
    #  \note If the creation fails, this function will ensure any errors are logged to the logging service.
    def createQmlComponent(
        self,
        qml_file_path: str,
        context_properties: Dict[str,
                                 "QObject"] = None) -> Optional["QObject"]:
        if self._qml_engine is None:  # Protect in case the engine was not initialized yet
            return None
        path = QUrl.fromLocalFile(qml_file_path)
        component = QQmlComponent(self._qml_engine, path)
        result_context = QQmlContext(
            self._qml_engine.rootContext()
        )  #type: ignore #MyPy doens't realise that self._qml_engine can't be None here.
        if context_properties is not None:
            for name, value in context_properties.items():
                result_context.setContextProperty(name, value)
        result = component.create(result_context)
        for err in component.errors():
            Logger.log("e", str(err.toString()))
        if result is None:
            return None

        # We need to store the context with the qml object, else the context gets garbage collected and the qml objects
        # no longer function correctly/application crashes.
        result.attached_context = result_context
        return result

    ##  Delete all nodes containing mesh data in the scene.
    #   \param only_selectable. Set this to False to delete objects from all build plates
    @pyqtSlot()
    def deleteAll(self, only_selectable=True) -> None:
        self.getController().deleteAllNodesWithMeshData(only_selectable)

    ##  Get the MeshFileHandler of this application.
    def getMeshFileHandler(self) -> MeshFileHandler:
        return self._mesh_file_handler

    def getWorkspaceFileHandler(self) -> WorkspaceFileHandler:
        return self._workspace_file_handler

    @pyqtSlot(result=QObject)
    def getPackageManager(self) -> PackageManager:
        return self._package_manager

    ##  Gets the instance of this application.
    #
    #   This is just to further specify the type of Application.getInstance().
    #   \return The instance of this application.
    @classmethod
    def getInstance(cls, *args, **kwargs) -> "QtApplication":
        return cast(QtApplication, super().getInstance(**kwargs))

    def _createSplashScreen(self) -> QSplashScreen:
        return QSplashScreen(
            QPixmap(
                Resources.getPath(Resources.Images,
                                  self.getApplicationName() + ".png")))

    def _screenScaleFactor(self) -> float:
        # OSX handles sizes of dialogs behind our backs, but other platforms need
        # to know about the device pixel ratio
        if sys.platform == "darwin":
            return 1.0
        else:
            # determine a device pixel ratio from font metrics, using the same logic as UM.Theme
            fontPixelRatio = QFontMetrics(
                QCoreApplication.instance().font()).ascent() / 11
            # round the font pixel ratio to quarters
            fontPixelRatio = int(fontPixelRatio * 4) / 4
            return fontPixelRatio

    @pyqtProperty(str, constant=True)
    def applicationDisplayName(self) -> str:
        return self.getApplicationDisplayName()
Beispiel #4
0
class TriblerWindow(QMainWindow):
    resize_event = pyqtSignal()
    escape_pressed = pyqtSignal()
    tribler_crashed = pyqtSignal(str)
    received_search_completions = pyqtSignal(object)

    def __init__(self, core_args=None, core_env=None, api_port=None, api_key=None):
        QMainWindow.__init__(self)
        self._logger = logging.getLogger(self.__class__.__name__)

        QCoreApplication.setOrganizationDomain("nl")
        QCoreApplication.setOrganizationName("TUDelft")
        QCoreApplication.setApplicationName("Tribler")

        self.setWindowIcon(QIcon(QPixmap(get_image_path('tribler.png'))))

        self.gui_settings = QSettings('nl.tudelft.tribler')
        api_port = api_port or int(get_gui_setting(self.gui_settings, "api_port", DEFAULT_API_PORT))
        api_key = api_key or get_gui_setting(self.gui_settings, "api_key", hexlify(os.urandom(16)).encode('utf-8'))
        self.gui_settings.setValue("api_key", api_key)

        api_port = get_first_free_port(start=api_port, limit=100)
        request_manager.port, request_manager.key = api_port, api_key

        self.tribler_started = False
        self.tribler_settings = None
        # TODO: move version_id to tribler_common and get core version in the core crash message
        self.tribler_version = version_id
        self.debug_window = None

        self.error_handler = ErrorHandler(self)
        self.core_manager = CoreManager(api_port, api_key, self.error_handler)
        self.pending_requests = {}
        self.pending_uri_requests = []
        self.download_uri = None
        self.dialog = None
        self.create_dialog = None
        self.chosen_dir = None
        self.new_version_dialog = None
        self.start_download_dialog_active = False
        self.selected_torrent_files = []
        self.has_search_results = False
        self.last_search_query = None
        self.last_search_time = None
        self.start_time = time.time()
        self.token_refresh_timer = None
        self.shutdown_timer = None
        self.add_torrent_url_dialog_active = False

        sys.excepthook = self.error_handler.gui_error

        uic.loadUi(get_ui_file_path('mainwindow.ui'), self)
        TriblerRequestManager.window = self
        self.tribler_status_bar.hide()

        self.token_balance_widget.mouseReleaseEvent = self.on_token_balance_click

        def on_state_update(new_state):
            self.loading_text_label.setText(new_state)

        connect(self.core_manager.core_state_update, on_state_update)

        self.magnet_handler = MagnetHandler(self.window)
        QDesktopServices.setUrlHandler("magnet", self.magnet_handler, "on_open_magnet_link")

        self.debug_pane_shortcut = QShortcut(QKeySequence("Ctrl+d"), self)
        connect(self.debug_pane_shortcut.activated, self.clicked_menu_button_debug)
        self.import_torrent_shortcut = QShortcut(QKeySequence("Ctrl+o"), self)
        connect(self.import_torrent_shortcut.activated, self.on_add_torrent_browse_file)
        self.add_torrent_url_shortcut = QShortcut(QKeySequence("Ctrl+i"), self)
        connect(self.add_torrent_url_shortcut.activated, self.on_add_torrent_from_url)

        connect(self.top_search_bar.clicked, self.clicked_search_bar)

        # Remove the focus rect on OS X
        for widget in self.findChildren(QLineEdit) + self.findChildren(QListWidget) + self.findChildren(QTreeWidget):
            widget.setAttribute(Qt.WA_MacShowFocusRect, 0)

        self.menu_buttons = [
            self.left_menu_button_downloads,
            self.left_menu_button_discovered,
            self.left_menu_button_trust_graph,
            self.left_menu_button_popular,
        ]
        hide_xxx = get_gui_setting(self.gui_settings, "family_filter", True, is_bool=True)
        self.search_results_page.initialize_content_page(hide_xxx=hide_xxx)
        self.search_results_page.channel_torrents_filter_input.setHidden(True)

        self.settings_page.initialize_settings_page()
        self.downloads_page.initialize_downloads_page()
        self.loading_page.initialize_loading_page()
        self.discovering_page.initialize_discovering_page()

        self.discovered_page.initialize_content_page(hide_xxx=hide_xxx)

        self.popular_page.initialize_content_page(hide_xxx=hide_xxx)

        self.trust_page.initialize_trust_page()
        self.trust_graph_page.initialize_trust_graph()

        self.stackedWidget.setCurrentIndex(PAGE_LOADING)

        # Create the system tray icon
        if QSystemTrayIcon.isSystemTrayAvailable():
            self.tray_icon = QSystemTrayIcon()
            use_monochrome_icon = get_gui_setting(self.gui_settings, "use_monochrome_icon", False, is_bool=True)
            self.update_tray_icon(use_monochrome_icon)

            # Create the tray icon menu
            menu = self.create_add_torrent_menu()
            show_downloads_action = QAction('Show downloads', self)
            connect(show_downloads_action.triggered, self.clicked_menu_button_downloads)
            token_balance_action = QAction('Show token balance', self)
            connect(token_balance_action.triggered, lambda _: self.on_token_balance_click(None))
            quit_action = QAction('Quit Tribler', self)
            connect(quit_action.triggered, self.close_tribler)
            menu.addSeparator()
            menu.addAction(show_downloads_action)
            menu.addAction(token_balance_action)
            menu.addSeparator()
            menu.addAction(quit_action)
            self.tray_icon.setContextMenu(menu)
        else:
            self.tray_icon = None

        self.left_menu_button_debug.setHidden(True)
        self.top_menu_button.setHidden(True)
        self.left_menu.setHidden(True)
        self.token_balance_widget.setHidden(True)
        self.settings_button.setHidden(True)
        self.add_torrent_button.setHidden(True)
        self.top_search_bar.setHidden(True)

        # Set various icons
        self.top_menu_button.setIcon(QIcon(get_image_path('menu.png')))

        self.search_completion_model = QStringListModel()
        completer = QCompleter()
        completer.setModel(self.search_completion_model)
        completer.setCompletionMode(QCompleter.UnfilteredPopupCompletion)
        self.item_delegate = QStyledItemDelegate()
        completer.popup().setItemDelegate(self.item_delegate)
        completer.popup().setStyleSheet(
            """
        QListView {
            background-color: #404040;
        }

        QListView::item {
            color: #D0D0D0;
            padding-top: 5px;
            padding-bottom: 5px;
        }

        QListView::item:hover {
            background-color: #707070;
        }
        """
        )
        self.top_search_bar.setCompleter(completer)

        # Toggle debug if developer mode is enabled
        self.window().left_menu_button_debug.setHidden(
            not get_gui_setting(self.gui_settings, "debug", False, is_bool=True)
        )

        # Start Tribler
        self.core_manager.start(core_args=core_args, core_env=core_env)

        connect(self.core_manager.events_manager.torrent_finished, self.on_torrent_finished)
        connect(self.core_manager.events_manager.new_version_available, self.on_new_version_available)
        connect(self.core_manager.events_manager.tribler_started, self.on_tribler_started)
        connect(self.core_manager.events_manager.low_storage_signal, self.on_low_storage)
        connect(self.core_manager.events_manager.tribler_shutdown_signal, self.on_tribler_shutdown_state_update)
        connect(self.core_manager.events_manager.config_error_signal, self.on_config_error_signal)

        # Install signal handler for ctrl+c events
        def sigint_handler(*_):
            self.close_tribler()

        signal.signal(signal.SIGINT, sigint_handler)

        # Resize the window according to the settings
        center = QApplication.desktop().availableGeometry(self).center()
        pos = self.gui_settings.value("pos", QPoint(center.x() - self.width() * 0.5, center.y() - self.height() * 0.5))
        size = self.gui_settings.value("size", self.size())

        self.move(pos)
        self.resize(size)

        self.show()

        self.add_to_channel_dialog = AddToChannelDialog(self.window())

        self.add_torrent_menu = self.create_add_torrent_menu()
        self.add_torrent_button.setMenu(self.add_torrent_menu)

        self.channels_menu_list = self.findChild(ChannelsMenuListWidget, "channels_menu_list")

        connect(self.channels_menu_list.itemClicked, self.open_channel_contents_page)

        # The channels content page is only used to show subscribed channels, so we always show xxx
        # contents in it.
        connect(
            self.core_manager.events_manager.node_info_updated,
            lambda data: self.channels_menu_list.reload_if_necessary([data]),
        )
        connect(self.left_menu_button_new_channel.clicked, self.create_new_channel)

    def create_new_channel(self, checked):
        # TODO: DRY this with tablecontentmodel, possibly using QActions

        def create_channel_callback(channel_name):
            TriblerNetworkRequest(
                "channels/mychannel/0/channels",
                self.channels_menu_list.load_channels,
                method='POST',
                raw_data=json.dumps({"name": channel_name}) if channel_name else None,
            )

        NewChannelDialog(self, create_channel_callback)

    def open_channel_contents_page(self, channel_list_item):
        if not channel_list_item.flags() & Qt.ItemIsEnabled:
            return

        self.channel_contents_page.initialize_root_model_from_channel_info(channel_list_item.channel_info)
        self.stackedWidget.setCurrentIndex(PAGE_CHANNEL_CONTENTS)
        self.deselect_all_menu_buttons()

    def update_tray_icon(self, use_monochrome_icon):
        if not QSystemTrayIcon.isSystemTrayAvailable() or not self.tray_icon:
            return

        if use_monochrome_icon:
            self.tray_icon.setIcon(QIcon(QPixmap(get_image_path('monochrome_tribler.png'))))
        else:
            self.tray_icon.setIcon(QIcon(QPixmap(get_image_path('tribler.png'))))
        self.tray_icon.show()

    def delete_tray_icon(self):
        if self.tray_icon:
            try:
                self.tray_icon.deleteLater()
            except RuntimeError:
                # The tray icon might have already been removed when unloading Qt.
                # This is due to the C code actually being asynchronous.
                logging.debug("Tray icon already removed, no further deletion necessary.")
            self.tray_icon = None

    def on_low_storage(self, _):
        """
        Dealing with low storage space available. First stop the downloads and the core manager and ask user to user to
        make free space.
        :return:
        """

        def close_tribler_gui():
            self.close_tribler()
            # Since the core has already stopped at this point, it will not terminate the GUI.
            # So, we quit the GUI separately here.
            if not QApplication.closingDown():
                QApplication.quit()

        self.downloads_page.stop_loading_downloads()
        self.core_manager.stop(False)
        close_dialog = ConfirmationDialog(
            self.window(),
            "<b>CRITICAL ERROR</b>",
            "You are running low on disk space (<100MB). Please make sure to have "
            "sufficient free space available and restart Tribler again.",
            [("Close Tribler", BUTTON_TYPE_NORMAL)],
        )
        connect(close_dialog.button_clicked, lambda _: close_tribler_gui())
        close_dialog.show()

    def on_torrent_finished(self, torrent_info):
        if "hidden" not in torrent_info or not torrent_info["hidden"]:
            self.tray_show_message("Download finished", f"Download of {torrent_info['name']} has finished.")

    def show_loading_screen(self):
        self.top_menu_button.setHidden(True)
        self.left_menu.setHidden(True)
        self.token_balance_widget.setHidden(True)
        self.settings_button.setHidden(True)
        self.add_torrent_button.setHidden(True)
        self.top_search_bar.setHidden(True)
        self.stackedWidget.setCurrentIndex(PAGE_LOADING)

    def tray_set_tooltip(self, message):
        """
        Set a tooltip message for the tray icon, if possible.

        :param message: the message to display on hover
        """
        if self.tray_icon:
            try:
                self.tray_icon.setToolTip(message)
            except RuntimeError as e:
                logging.error("Failed to set tray tooltip: %s", str(e))

    def tray_show_message(self, title, message):
        """
        Show a message at the tray icon, if possible.

        :param title: the title of the message
        :param message: the message to display
        """
        if self.tray_icon:
            try:
                self.tray_icon.showMessage(title, message)
            except RuntimeError as e:
                logging.error("Failed to set tray message: %s", str(e))

    def on_tribler_started(self, version):
        if self.tribler_started:
            logging.warning("Received duplicate Tribler Core started event")
            return

        self.tribler_started = True
        self.tribler_version = version

        self.top_menu_button.setHidden(False)
        self.left_menu.setHidden(False)
        self.token_balance_widget.setHidden(False)
        self.settings_button.setHidden(False)
        self.add_torrent_button.setHidden(False)
        self.top_search_bar.setHidden(False)

        self.fetch_settings()

        self.downloads_page.start_loading_downloads()

        self.setAcceptDrops(True)
        self.setWindowTitle(f"Tribler {self.tribler_version}")

        autocommit_enabled = (
            get_gui_setting(self.gui_settings, "autocommit_enabled", True, is_bool=True) if self.gui_settings else True
        )
        self.channel_contents_page.initialize_content_page(autocommit_enabled=autocommit_enabled, hide_xxx=False)

        hide_xxx = get_gui_setting(self.gui_settings, "family_filter", True, is_bool=True)
        self.discovered_page.initialize_root_model(
            DiscoveredChannelsModel(
                channel_info={"name": "Discovered channels"}, endpoint_url="channels", hide_xxx=hide_xxx
            )
        )
        connect(self.core_manager.events_manager.discovered_channel, self.discovered_page.model.on_new_entry_received)

        self.popular_page.initialize_root_model(
            PopularTorrentsModel(channel_info={"name": "Popular torrents"}, hide_xxx=hide_xxx)
        )

        self.add_to_channel_dialog.load_channel(0)

        if not self.gui_settings.value("first_discover", False) and not self.core_manager.use_existing_core:
            connect(self.core_manager.events_manager.discovered_channel, self.stop_discovering)
            self.window().gui_settings.setValue("first_discover", True)
            self.discovering_page.is_discovering = True
            self.stackedWidget.setCurrentIndex(PAGE_DISCOVERING)
        else:
            self.clicked_menu_button_discovered()
            self.left_menu_button_discovered.setChecked(True)

        self.channels_menu_list.load_channels()

    def stop_discovering(self, response):
        if not self.discovering_page.is_discovering:
            return
        disconnect(self.core_manager.events_manager.discovered_channel, self.stop_discovering)
        self.discovering_page.is_discovering = False
        if self.stackedWidget.currentIndex() == PAGE_DISCOVERING:
            self.clicked_menu_button_discovered()
            self.left_menu_button_discovered.setChecked(True)

    def on_events_started(self, json_dict):
        self.setWindowTitle(f"Tribler {json_dict['version']}")

    def show_status_bar(self, message):
        self.tribler_status_bar_label.setText(message)
        self.tribler_status_bar.show()

    def hide_status_bar(self):
        self.tribler_status_bar.hide()

    def process_uri_request(self):
        """
        Process a URI request if we have one in the queue.
        """
        if len(self.pending_uri_requests) == 0:
            return

        uri = self.pending_uri_requests.pop()

        if uri.startswith('file') or uri.startswith('magnet'):
            self.start_download_from_uri(uri)

    def update_recent_download_locations(self, destination):
        # Save the download location to the GUI settings
        current_settings = get_gui_setting(self.gui_settings, "recent_download_locations", "")
        recent_locations = current_settings.split(",") if len(current_settings) > 0 else []
        if isinstance(destination, str):
            destination = destination.encode('utf-8')
        encoded_destination = hexlify(destination)
        if encoded_destination in recent_locations:
            recent_locations.remove(encoded_destination)
        recent_locations.insert(0, encoded_destination)

        if len(recent_locations) > 5:
            recent_locations = recent_locations[:5]

        self.gui_settings.setValue("recent_download_locations", ','.join(recent_locations))

    def perform_start_download_request(
        self,
        uri,
        anon_download,
        safe_seeding,
        destination,
        selected_files,
        total_files=0,
        add_to_channel=False,
        callback=None,
    ):
        # Check if destination directory is writable
        is_writable, error = is_dir_writable(destination)
        if not is_writable:
            gui_error_message = (
                "Insufficient write permissions to <i>%s</i> directory. Please add proper "
                "write permissions on the directory and add the torrent again. %s" % (destination, error)
            )
            ConfirmationDialog.show_message(self.window(), f"Download error <i>{uri}</i>", gui_error_message, "OK")
            return

        selected_files_list = []
        if len(selected_files) != total_files:  # Not all files included
            selected_files_list = [filename for filename in selected_files]

        anon_hops = int(self.tribler_settings['download_defaults']['number_hops']) if anon_download else 0
        safe_seeding = 1 if safe_seeding else 0
        post_data = {
            "uri": uri,
            "anon_hops": anon_hops,
            "safe_seeding": safe_seeding,
            "destination": destination,
            "selected_files": selected_files_list,
        }
        TriblerNetworkRequest(
            "downloads", callback if callback else self.on_download_added, method='PUT', data=post_data
        )

        self.update_recent_download_locations(destination)

        if add_to_channel:

            def on_add_button_pressed(channel_id):
                post_data = {}
                if uri.startswith("file:"):
                    with open(uri[5:], "rb") as torrent_file:
                        post_data['torrent'] = b64encode(torrent_file.read()).decode('utf8')
                elif uri.startswith("magnet:"):
                    post_data['uri'] = uri

                if post_data:
                    TriblerNetworkRequest(
                        f"channels/mychannel/{channel_id}/torrents",
                        lambda _: self.tray_show_message("Channel update", "Torrent(s) added to your channel"),
                        method='PUT',
                        data=post_data,
                    )

            self.window().add_to_channel_dialog.show_dialog(on_add_button_pressed, confirm_button_text="Add torrent")

    def on_new_version_available(self, version):
        if version == str(self.gui_settings.value('last_reported_version')):
            return

        # To prevent multiple dialogs on top of each other,
        # close any existing dialog first.
        if self.new_version_dialog:
            self.new_version_dialog.close_dialog()
            self.new_version_dialog = None

        self.new_version_dialog = ConfirmationDialog(
            self,
            "New version available",
            "Version %s of Tribler is available.Do you want to visit the "
            "website to download the newest version?" % version,
            [('IGNORE', BUTTON_TYPE_NORMAL), ('LATER', BUTTON_TYPE_NORMAL), ('OK', BUTTON_TYPE_NORMAL)],
        )
        connect(self.new_version_dialog.button_clicked, lambda action: self.on_new_version_dialog_done(version, action))
        self.new_version_dialog.show()

    def on_new_version_dialog_done(self, version, action):
        if action == 0:  # ignore
            self.gui_settings.setValue("last_reported_version", version)
        elif action == 2:  # ok
            import webbrowser

            webbrowser.open("https://tribler.org")
        if self.new_version_dialog:
            self.new_version_dialog.close_dialog()
            self.new_version_dialog = None

    def on_search_text_change(self, text):
        # We do not want to bother the database on petty 1-character queries
        if len(text) < 2:
            return
        TriblerNetworkRequest(
            "search/completions", self.on_received_search_completions, url_params={'q': sanitize_for_fts(text)}
        )

    def on_received_search_completions(self, completions):
        if completions is None:
            return
        self.received_search_completions.emit(completions)
        self.search_completion_model.setStringList(completions["completions"])

    def fetch_settings(self):
        TriblerNetworkRequest("settings", self.received_settings, capture_core_errors=False)

    def received_settings(self, settings):
        if not settings:
            return
        # If we cannot receive the settings, stop Tribler with an option to send the crash report.
        if 'error' in settings:
            raise RuntimeError(TriblerRequestManager.get_message_from_error(settings))

        # If there is any pending dialog (likely download dialog or error dialog of setting not available),
        # close the dialog
        if self.dialog:
            self.dialog.close_dialog()
            self.dialog = None

        self.tribler_settings = settings['settings']

        self.downloads_all_button.click()

        # process pending file requests (i.e. someone clicked a torrent file when Tribler was closed)
        # We do this after receiving the settings so we have the default download location.
        self.process_uri_request()

        # Set token balance refresh timer and load the token balance
        self.token_refresh_timer = QTimer()
        connect(self.token_refresh_timer.timeout, self.load_token_balance)
        self.token_refresh_timer.start(60000)

        self.load_token_balance()

    def on_top_search_button_click(self):
        current_ts = time.time()
        query = self.top_search_bar.text()

        if (
            self.last_search_query
            and self.last_search_time
            and self.last_search_query == self.top_search_bar.text()
            and current_ts - self.last_search_time < 1
        ):
            logging.info("Same search query already sent within 500ms so dropping this one")
            return

        if not query:
            return

        self.has_search_results = True
        self.search_results_page.initialize_root_model(
            SearchResultsModel(
                channel_info={"name": f"Search results for {query}" if len(query) < 50 else f"{query[:50]}..."},
                endpoint_url="search",
                hide_xxx=get_gui_setting(self.gui_settings, "family_filter", True, is_bool=True),
                text_filter=to_fts_query(query),
            )
        )

        self.clicked_search_bar()

        # Trigger remote search
        self.search_results_page.preview_clicked()

        self.last_search_query = query
        self.last_search_time = current_ts

    def on_settings_button_click(self):
        self.deselect_all_menu_buttons()
        self.stackedWidget.setCurrentIndex(PAGE_SETTINGS)
        self.settings_page.load_settings()

    def on_token_balance_click(self, _):
        self.raise_window()
        self.deselect_all_menu_buttons()
        self.stackedWidget.setCurrentIndex(PAGE_TRUST)
        self.load_token_balance()
        self.trust_page.load_history()

    def load_token_balance(self):
        TriblerNetworkRequest("bandwidth/statistics", self.received_bandwidth_statistics, capture_core_errors=False)

    def received_bandwidth_statistics(self, statistics):
        if not statistics or "statistics" not in statistics:
            return

        self.trust_page.received_bandwidth_statistics(statistics)

        statistics = statistics["statistics"]
        balance = statistics["total_given"] - statistics["total_taken"]
        self.set_token_balance(balance)

        # If trust page is currently visible, then load the graph as well
        if self.stackedWidget.currentIndex() == PAGE_TRUST:
            self.trust_page.load_history()

    def set_token_balance(self, balance):
        if abs(balance) > 1024 ** 4:  # Balance is over a TB
            balance /= 1024.0 ** 4
            self.token_balance_label.setText(f"{balance:.1f} TB")
        elif abs(balance) > 1024 ** 3:  # Balance is over a GB
            balance /= 1024.0 ** 3
            self.token_balance_label.setText(f"{balance:.1f} GB")
        else:
            balance /= 1024.0 ** 2
            self.token_balance_label.setText("%d MB" % balance)

    def raise_window(self):
        self.setWindowState(self.windowState() & ~Qt.WindowMinimized | Qt.WindowActive)
        self.raise_()
        self.activateWindow()

    def create_add_torrent_menu(self):
        """
        Create a menu to add new torrents. Shows when users click on the tray icon or the big plus button.
        """
        menu = TriblerActionMenu(self)

        browse_files_action = QAction('Import torrent from file', self)
        browse_directory_action = QAction('Import torrent(s) from directory', self)
        add_url_action = QAction('Import torrent from magnet/URL', self)
        create_torrent_action = QAction('Create torrent from file(s)', self)

        connect(browse_files_action.triggered, self.on_add_torrent_browse_file)
        connect(browse_directory_action.triggered, self.on_add_torrent_browse_dir)
        connect(add_url_action.triggered, self.on_add_torrent_from_url)
        connect(create_torrent_action.triggered, self.on_create_torrent)

        menu.addAction(browse_files_action)
        menu.addAction(browse_directory_action)
        menu.addAction(add_url_action)
        menu.addSeparator()
        menu.addAction(create_torrent_action)

        return menu

    def on_create_torrent(self, checked):
        if self.create_dialog:
            self.create_dialog.close_dialog()

        self.create_dialog = CreateTorrentDialog(self)
        connect(self.create_dialog.create_torrent_notification, self.on_create_torrent_updates)
        self.create_dialog.show()

    def on_create_torrent_updates(self, update_dict):
        self.tray_show_message("Torrent updates", update_dict['msg'])

    def on_add_torrent_browse_file(self, index):
        filenames = QFileDialog.getOpenFileNames(
            self, "Please select the .torrent file", QDir.homePath(), "Torrent files (*.torrent)"
        )
        if len(filenames[0]) > 0:
            for filename in filenames[0]:
                self.pending_uri_requests.append(f"file:{filename}")
            self.process_uri_request()

    def start_download_from_uri(self, uri):
        uri = uri.decode('utf-8') if isinstance(uri, bytes) else uri
        self.download_uri = uri

        if get_gui_setting(self.gui_settings, "ask_download_settings", True, is_bool=True):
            # FIXME: instead of using this workaround, make sure the settings are _available_ by this moment
            # If tribler settings is not available, fetch the settings and inform the user to try again.
            if not self.tribler_settings:
                self.fetch_settings()
                self.dialog = ConfirmationDialog.show_error(
                    self,
                    "Download Error",
                    "Tribler settings is not available\
                                                                   yet. Fetching it now. Please try again later.",
                )
                # By re-adding the download uri to the pending list, the request is re-processed
                # when the settings is received
                self.pending_uri_requests.append(uri)
                return
            # Clear any previous dialog if exists
            if self.dialog:
                self.dialog.close_dialog()
                self.dialog = None

            self.dialog = StartDownloadDialog(self, self.download_uri)
            connect(self.dialog.button_clicked, self.on_start_download_action)
            self.dialog.show()
            self.start_download_dialog_active = True
        else:
            # FIXME: instead of using this workaround, make sure the settings are _available_ by this moment
            # In the unlikely scenario that tribler settings are not available yet, try to fetch settings again and
            # add the download uri back to self.pending_uri_requests to process again.
            if not self.tribler_settings:
                self.fetch_settings()
                if self.download_uri not in self.pending_uri_requests:
                    self.pending_uri_requests.append(self.download_uri)
                return

            self.window().perform_start_download_request(
                self.download_uri,
                self.window().tribler_settings['download_defaults']['anonymity_enabled'],
                self.window().tribler_settings['download_defaults']['safeseeding_enabled'],
                self.tribler_settings['download_defaults']['saveas'],
                [],
            )
            self.process_uri_request()

    def on_start_download_action(self, action):
        if action == 1:
            if self.dialog and self.dialog.dialog_widget:
                self.window().perform_start_download_request(
                    self.download_uri,
                    self.dialog.dialog_widget.anon_download_checkbox.isChecked(),
                    self.dialog.dialog_widget.safe_seed_checkbox.isChecked(),
                    self.dialog.dialog_widget.destination_input.currentText(),
                    self.dialog.get_selected_files(),
                    self.dialog.dialog_widget.files_list_view.topLevelItemCount(),
                    add_to_channel=self.dialog.dialog_widget.add_to_channel_checkbox.isChecked(),
                )
            else:
                ConfirmationDialog.show_error(self, "Tribler UI Error", "Something went wrong. Please try again.")
                logging.exception("Error while trying to download. Either dialog or dialog.dialog_widget is None")

        if self.dialog:
            self.dialog.close_dialog()
            self.dialog = None
            self.start_download_dialog_active = False

        if action == 0:  # We do this after removing the dialog since process_uri_request is blocking
            self.process_uri_request()

    def on_add_torrent_browse_dir(self, checked):
        chosen_dir = QFileDialog.getExistingDirectory(
            self, "Please select the directory containing the .torrent files", QDir.homePath(), QFileDialog.ShowDirsOnly
        )
        self.chosen_dir = chosen_dir
        if len(chosen_dir) != 0:
            self.selected_torrent_files = [torrent_file for torrent_file in glob.glob(chosen_dir + "/*.torrent")]
            self.dialog = ConfirmationDialog(
                self,
                "Add torrents from directory",
                "Add %s torrent files from the following directory "
                "to your Tribler channel:\n\n%s" % (len(self.selected_torrent_files), chosen_dir),
                [('ADD', BUTTON_TYPE_NORMAL), ('CANCEL', BUTTON_TYPE_CONFIRM)],
                checkbox_text="Add torrents to My Channel",
            )
            connect(self.dialog.button_clicked, self.on_confirm_add_directory_dialog)
            self.dialog.show()

    def on_confirm_add_directory_dialog(self, action):
        if action == 0:
            if self.dialog.checkbox.isChecked():
                # TODO: add recursive directory scanning
                def on_add_button_pressed(channel_id):
                    TriblerNetworkRequest(
                        f"collections/mychannel/{channel_id}/torrents",
                        lambda _: self.tray_show_message("Channels update", f"{self.chosen_dir} added to your channel"),
                        method='PUT',
                        data={"torrents_dir": self.chosen_dir},
                    )

                self.window().add_to_channel_dialog.show_dialog(
                    on_add_button_pressed, confirm_button_text="Add torrent(s)"
                )

            for torrent_file in self.selected_torrent_files:
                self.perform_start_download_request(
                    f"file:{torrent_file}",
                    self.window().tribler_settings['download_defaults']['anonymity_enabled'],
                    self.window().tribler_settings['download_defaults']['safeseeding_enabled'],
                    self.tribler_settings['download_defaults']['saveas'],
                    [],
                )

        if self.dialog:
            self.dialog.close_dialog()
            self.dialog = None

    def on_add_torrent_from_url(self, checked=False):
        # Make sure that the window is visible (this action might be triggered from the tray icon)
        self.raise_window()

        if not self.add_torrent_url_dialog_active:
            self.dialog = ConfirmationDialog(
                self,
                "Add torrent from URL/magnet link",
                "Please enter the URL/magnet link in the field below:",
                [('ADD', BUTTON_TYPE_NORMAL), ('CANCEL', BUTTON_TYPE_CONFIRM)],
                show_input=True,
            )
            self.dialog.dialog_widget.dialog_input.setPlaceholderText('URL/magnet link')
            self.dialog.dialog_widget.dialog_input.setFocus()
            connect(self.dialog.button_clicked, self.on_torrent_from_url_dialog_done)
            self.dialog.show()
            self.add_torrent_url_dialog_active = True

    def on_torrent_from_url_dialog_done(self, action):
        self.add_torrent_url_dialog_active = False
        if self.dialog and self.dialog.dialog_widget:
            uri = self.dialog.dialog_widget.dialog_input.text().strip()

            # If the URI is a 40-bytes hex-encoded infohash, convert it to a valid magnet link
            if len(uri) == 40:
                valid_ih_hex = True
                try:
                    int(uri, 16)
                except ValueError:
                    valid_ih_hex = False

                if valid_ih_hex:
                    uri = "magnet:?xt=urn:btih:" + uri

            # Remove first dialog
            self.dialog.close_dialog()
            self.dialog = None

            if action == 0:
                self.start_download_from_uri(uri)

    def on_download_added(self, result):
        if not result:
            return
        if len(self.pending_uri_requests) == 0:  # Otherwise, we first process the remaining requests.
            self.window().left_menu_button_downloads.click()
        else:
            self.process_uri_request()

    def on_top_menu_button_click(self):
        if self.left_menu.isHidden():
            self.left_menu.show()
        else:
            self.left_menu.hide()

    def deselect_all_menu_buttons(self, except_select=None):
        for button in self.menu_buttons:
            if button == except_select:
                button.setEnabled(False)
                continue
            button.setEnabled(True)
            button.setChecked(False)

    def clicked_search_bar(self, checked=False):
        query = self.top_search_bar.text()
        if query and self.has_search_results:
            self.deselect_all_menu_buttons()
            if self.stackedWidget.currentIndex() == PAGE_SEARCH_RESULTS:
                self.search_results_page.go_back_to_level(0)
            self.stackedWidget.setCurrentIndex(PAGE_SEARCH_RESULTS)

    def clicked_menu_button_discovered(self):
        self.deselect_all_menu_buttons()
        self.left_menu_button_discovered.setChecked(True)
        if self.stackedWidget.currentIndex() == PAGE_DISCOVERED:
            self.discovered_page.go_back_to_level(0)
            self.discovered_page.reset_view()
        self.stackedWidget.setCurrentIndex(PAGE_DISCOVERED)
        self.discovered_page.content_table.setFocus()

    def clicked_menu_button_popular(self):
        self.deselect_all_menu_buttons()
        self.left_menu_button_popular.setChecked(True)
        # We want to reset the view every time to show updates
        self.popular_page.go_back_to_level(0)
        self.popular_page.reset_view()
        self.stackedWidget.setCurrentIndex(PAGE_POPULAR)
        self.popular_page.content_table.setFocus()

    def clicked_menu_button_trust_graph(self):
        self.deselect_all_menu_buttons(self.left_menu_button_trust_graph)
        self.stackedWidget.setCurrentIndex(PAGE_TRUST_GRAPH_PAGE)

    def clicked_menu_button_downloads(self, checked):
        self.deselect_all_menu_buttons(self.left_menu_button_downloads)
        self.raise_window()
        self.left_menu_button_downloads.setChecked(True)
        self.stackedWidget.setCurrentIndex(PAGE_DOWNLOADS)

    def clicked_menu_button_debug(self, index=False):
        if not self.debug_window:
            self.debug_window = DebugWindow(self.tribler_settings, self.tribler_version)
        self.debug_window.show()

    def resizeEvent(self, _):
        # This thing here is necessary to send the resize event to dialogs, etc.
        self.resize_event.emit()

    def close_tribler(self, checked=False):
        if self.core_manager.shutting_down:
            return

        def show_force_shutdown():
            self.window().force_shutdown_btn.show()

        self.delete_tray_icon()
        self.show_loading_screen()
        self.hide_status_bar()
        self.loading_text_label.setText("Shutting down...")
        if self.debug_window:
            self.debug_window.setHidden(True)

        self.shutdown_timer = QTimer()
        connect(self.shutdown_timer.timeout, show_force_shutdown)
        self.shutdown_timer.start(SHUTDOWN_WAITING_PERIOD)

        self.gui_settings.setValue("pos", self.pos())
        self.gui_settings.setValue("size", self.size())

        if self.core_manager.use_existing_core:
            # Don't close the core that we are using
            QApplication.quit()

        self.core_manager.stop()
        self.core_manager.shutting_down = True
        self.downloads_page.stop_loading_downloads()
        request_manager.clear()

        # Stop the token balance timer
        if self.token_refresh_timer:
            self.token_refresh_timer.stop()

    def closeEvent(self, close_event):
        self.close_tribler()
        close_event.ignore()

    def dragEnterEvent(self, e):
        file_urls = [_qurl_to_path(url) for url in e.mimeData().urls()] if e.mimeData().hasUrls() else []

        if any(os.path.isfile(filename) for filename in file_urls):
            e.accept()
        else:
            e.ignore()

    def dropEvent(self, e):
        file_urls = (
            [(_qurl_to_path(url), url.toString()) for url in e.mimeData().urls()] if e.mimeData().hasUrls() else []
        )

        for filename, fileurl in file_urls:
            if os.path.isfile(filename):
                self.start_download_from_uri(fileurl)

        e.accept()

    def clicked_force_shutdown(self):
        process_checker = ProcessChecker()
        if process_checker.already_running:
            core_pid = process_checker.get_pid_from_lock_file()
            os.kill(int(core_pid), 9)
        # Stop the Qt application
        QApplication.quit()

    def clicked_skip_conversion(self):
        self.dialog = ConfirmationDialog(
            self,
            "Abort the conversion of Channels database",
            "The upgrade procedure is now <b>converting your personal channel</b> and channels "
            "collected by the previous installation of Tribler.<br>"
            "Are you sure you want to abort the conversion process?<br><br>"
            "<p style='color:red'><b> !!! WARNING !!! <br>"
            "You will lose your personal channel and subscribed channels if you ABORT now! </b> </p> <br>",
            [('ABORT', BUTTON_TYPE_CONFIRM), ('CONTINUE', BUTTON_TYPE_NORMAL)],
        )
        connect(self.dialog.button_clicked, self.on_skip_conversion_dialog)
        self.dialog.show()

    def on_channel_subscribe(self, channel_info):
        patch_data = [{"public_key": channel_info['public_key'], "id": channel_info['id'], "subscribed": True}]
        TriblerNetworkRequest(
            "metadata",
            lambda data: self.core_manager.events_manager.node_info_updated.emit(data[0]),
            raw_data=json.dumps(patch_data),
            method='PATCH',
        )

    def on_channel_unsubscribe(self, channel_info):
        def _on_unsubscribe_action(action):
            if action == 0:
                patch_data = [{"public_key": channel_info['public_key'], "id": channel_info['id'], "subscribed": False}]
                TriblerNetworkRequest(
                    "metadata",
                    lambda data: self.core_manager.events_manager.node_info_updated.emit(data[0]),
                    raw_data=json.dumps(patch_data),
                    method='PATCH',
                )
            if self.dialog:
                self.dialog.close_dialog()
                self.dialog = None

        self.dialog = ConfirmationDialog(
            self,
            "Unsubscribe from channel",
            "Are you sure you want to <b>unsubscribe</b> from channel<br/>"
            + '\"'
            + f"<b>{channel_info['name']}</b>"
            + '\"'
            + "<br/>and remove its contents?",
            [('UNSUBSCRIBE', BUTTON_TYPE_NORMAL), ('CANCEL', BUTTON_TYPE_CONFIRM)],
        )
        connect(self.dialog.button_clicked, _on_unsubscribe_action)
        self.dialog.show()

    def on_channel_delete(self, channel_info):
        def _on_delete_action(action):
            if action == 0:
                delete_data = [{"public_key": channel_info['public_key'], "id": channel_info['id']}]
                TriblerNetworkRequest(
                    "metadata",
                    lambda data: self.core_manager.events_manager.node_info_updated.emit(data[0]),
                    raw_data=json.dumps(delete_data),
                    method='DELETE',
                )
            if self.dialog:
                self.dialog.close_dialog()
                self.dialog = None

        self.dialog = ConfirmationDialog(
            self,
            "Delete channel",
            "Are you sure you want to <b>delete</b> your personal channel<br/>"
            + '\"'
            + f"<b>{channel_info['name']}</b>"
            + '\"'
            + "<br/>and all its contents?",
            [('DELETE', BUTTON_TYPE_NORMAL), ('CANCEL', BUTTON_TYPE_CONFIRM)],
        )
        connect(self.dialog.button_clicked, _on_delete_action)
        self.dialog.show()

    def on_skip_conversion_dialog(self, action):
        if action == 0:
            TriblerNetworkRequest("upgrader", lambda _: None, data={"skip_db_upgrade": True}, method='POST')

        if self.dialog:
            self.dialog.close_dialog()
            self.dialog = None

    def on_tribler_shutdown_state_update(self, state):
        self.loading_text_label.setText(state)

    def on_config_error_signal(self, stacktrace):
        self._logger.error(f"Config error: {stacktrace}")
        user_message = 'Tribler recovered from a corrupted config. Please check your settings and update if necessary.'
        ConfirmationDialog.show_error(self, "Tribler config error", user_message)
Beispiel #5
0
class QtApplication(QApplication, Application):
    pluginsLoaded = Signal()
    applicationRunning = Signal()
    
    def __init__(self, tray_icon_name: str = None, **kwargs) -> None:
        plugin_path = ""
        if sys.platform == "win32":
            if hasattr(sys, "frozen"):
                plugin_path = os.path.join(os.path.dirname(os.path.abspath(sys.executable)), "PyQt5", "plugins")
                Logger.log("i", "Adding QT5 plugin path: %s", plugin_path)
                QCoreApplication.addLibraryPath(plugin_path)
            else:
                import site
                for sitepackage_dir in site.getsitepackages():
                    QCoreApplication.addLibraryPath(os.path.join(sitepackage_dir, "PyQt5", "plugins"))
        elif sys.platform == "darwin":
            plugin_path = os.path.join(self.getInstallPrefix(), "Resources", "plugins")

        if plugin_path:
            Logger.log("i", "Adding QT5 plugin path: %s", plugin_path)
            QCoreApplication.addLibraryPath(plugin_path)

        # use Qt Quick Scene Graph "basic" render loop
        os.environ["QSG_RENDER_LOOP"] = "basic"

        super().__init__(sys.argv, **kwargs) # type: ignore

        self._qml_import_paths = [] #type: List[str]
        self._main_qml = "main.qml" #type: str
        self._qml_engine = None #type: Optional[QQmlApplicationEngine]
        self._main_window = None #type: Optional[MainWindow]
        self._tray_icon_name = tray_icon_name #type: Optional[str]
        self._tray_icon = None #type: Optional[str]
        self._tray_icon_widget = None #type: Optional[QSystemTrayIcon]
        self._theme = None #type: Optional[Theme]
        self._renderer = None #type: Optional[QtRenderer]

        self._job_queue = None #type: Optional[JobQueue]
        self._version_upgrade_manager = None #type: Optional[VersionUpgradeManager]

        self._is_shutting_down = False #type: bool

        self._recent_files = [] #type: List[QUrl]

        self._configuration_error_message = None #type: Optional[ConfigurationErrorMessage]

    def addCommandLineOptions(self) -> None:
        super().addCommandLineOptions()
        # This flag is used by QApplication. We don't process it.
        self._cli_parser.add_argument("-qmljsdebugger",
                                      help = "For Qt's QML debugger compatibility")

    def initialize(self) -> None:
        super().initialize()
        # Initialize the package manager to remove and install scheduled packages.
        self._package_manager = self._package_manager_class(self, parent = self)

        self._mesh_file_handler = MeshFileHandler(self) #type: MeshFileHandler
        self._workspace_file_handler = WorkspaceFileHandler(self) #type: WorkspaceFileHandler

        # Remove this and you will get Windows 95 style for all widgets if you are using Qt 5.10+
        self.setStyle("fusion")

        self.setAttribute(Qt.AA_UseDesktopOpenGL)
        major_version, minor_version, profile = OpenGLContext.detectBestOpenGLVersion()

        if major_version is None and minor_version is None and profile is None and not self.getIsHeadLess():
            Logger.log("e", "Startup failed because OpenGL version probing has failed: tried to create a 2.0 and 4.1 context. Exiting")
            QMessageBox.critical(None, "Failed to probe OpenGL",
                                 "Could not probe OpenGL. This program requires OpenGL 2.0 or higher. Please check your video card drivers.")
            sys.exit(1)
        else:
            opengl_version_str = OpenGLContext.versionAsText(major_version, minor_version, profile)
            Logger.log("d", "Detected most suitable OpenGL context version: %s", opengl_version_str)
        if not self.getIsHeadLess():
            OpenGLContext.setDefaultFormat(major_version, minor_version, profile = profile)

        self._qml_import_paths.append(os.path.join(os.path.dirname(sys.executable), "qml"))
        self._qml_import_paths.append(os.path.join(self.getInstallPrefix(), "Resources", "qml"))

        Logger.log("i", "Initializing job queue ...")
        self._job_queue = JobQueue()
        self._job_queue.jobFinished.connect(self._onJobFinished)

        Logger.log("i", "Initializing version upgrade manager ...")
        self._version_upgrade_manager = VersionUpgradeManager(self)

    def startSplashWindowPhase(self) -> None:
        super().startSplashWindowPhase()

        self._package_manager.initialize()

        # Read preferences here (upgrade won't work) to get the language in use, so the splash window can be shown in
        # the correct language.
        try:
            preferences_filename = Resources.getPath(Resources.Preferences, self._app_name + ".cfg")
            self._preferences.readFromFile(preferences_filename)
        except FileNotFoundError:
            Logger.log("i", "Preferences file not found, ignore and use default language '%s'", self._default_language)

        signal.signal(signal.SIGINT, signal.SIG_DFL)
        # This is done here as a lot of plugins require a correct gl context. If you want to change the framework,
        # these checks need to be done in your <framework>Application.py class __init__().

        i18n_catalog = i18nCatalog("uranium")

        self._configuration_error_message = ConfigurationErrorMessage(self,
              i18n_catalog.i18nc("@info:status", "Your configuration seems to be corrupt."),
              lifetime = 0,
              title = i18n_catalog.i18nc("@info:title", "Configuration errors")
              )
        # Remove, install, and then loading plugins
        self.showSplashMessage(i18n_catalog.i18nc("@info:progress", "Loading plugins..."))
        # Remove and install the plugins that have been scheduled
        self._plugin_registry.initializeBeforePluginsAreLoaded()
        self._loadPlugins()
        self._plugin_registry.checkRequiredPlugins(self.getRequiredPlugins())
        self.pluginsLoaded.emit()

        self.showSplashMessage(i18n_catalog.i18nc("@info:progress", "Updating configuration..."))
        with self._container_registry.lockFile():
            VersionUpgradeManager.getInstance().upgrade()

        # Load preferences again because before we have loaded the plugins, we don't have the upgrade routine for
        # the preferences file. Now that we have, load the preferences file again so it can be upgraded and loaded.
        try:
            preferences_filename = Resources.getPath(Resources.Preferences, self._app_name + ".cfg")
            with open(preferences_filename, "r", encoding = "utf-8") as f:
                serialized = f.read()
            # This performs the upgrade for Preferences
            self._preferences.deserialize(serialized)
            self._preferences.setValue("general/plugins_to_remove", "")
            self._preferences.writeToFile(preferences_filename)
        except FileNotFoundError:
            Logger.log("i", "The preferences file cannot be found, will use default values")

        # Force the configuration file to be written again since the list of plugins to remove maybe changed
        self.showSplashMessage(i18n_catalog.i18nc("@info:progress", "Loading preferences..."))
        try:
            self._preferences_filename = Resources.getPath(Resources.Preferences, self._app_name + ".cfg")
            self._preferences.readFromFile(self._preferences_filename)
        except FileNotFoundError:
            Logger.log("i", "The preferences file '%s' cannot be found, will use default values",
                       self._preferences_filename)
            self._preferences_filename = Resources.getStoragePath(Resources.Preferences, self._app_name + ".cfg")

        # FIXME: This is done here because we now use "plugins.json" to manage plugins instead of the Preferences file,
        # but the PluginRegistry will still import data from the Preferences files if present, such as disabled plugins,
        # so we need to reset those values AFTER the Preferences file is loaded.
        self._plugin_registry.initializeAfterPluginsAreLoaded()

        # Check if we have just updated from an older version
        self._preferences.addPreference("general/last_run_version", "")
        last_run_version_str = self._preferences.getValue("general/last_run_version")
        if not last_run_version_str:
            last_run_version_str = self._version
        last_run_version = Version(last_run_version_str)
        current_version = Version(self._version)
        if last_run_version < current_version:
            self._just_updated_from_old_version = True
        self._preferences.setValue("general/last_run_version", str(current_version))
        self._preferences.writeToFile(self._preferences_filename)

        # Preferences: recent files
        self._preferences.addPreference("%s/recent_files" % self._app_name, "")
        file_names = self._preferences.getValue("%s/recent_files" % self._app_name).split(";")
        for file_name in file_names:
            if not os.path.isfile(file_name):
                continue
            self._recent_files.append(QUrl.fromLocalFile(file_name))

        if not self.getIsHeadLess():
            # Initialize System tray icon and make it invisible because it is used only to show pop up messages
            self._tray_icon = None
            if self._tray_icon_name:
                self._tray_icon = QIcon(Resources.getPath(Resources.Images, self._tray_icon_name))
                self._tray_icon_widget = QSystemTrayIcon(self._tray_icon)
                self._tray_icon_widget.setVisible(False)

    def initializeEngine(self) -> None:
        # TODO: Document native/qml import trickery
        self._qml_engine = QQmlApplicationEngine(self)
        self._qml_engine.setOutputWarningsToStandardError(False)
        self._qml_engine.warnings.connect(self.__onQmlWarning)

        for path in self._qml_import_paths:
            self._qml_engine.addImportPath(path)

        if not hasattr(sys, "frozen"):
            self._qml_engine.addImportPath(os.path.join(os.path.dirname(__file__), "qml"))

        self._qml_engine.rootContext().setContextProperty("QT_VERSION_STR", QT_VERSION_STR)
        self._qml_engine.rootContext().setContextProperty("screenScaleFactor", self._screenScaleFactor())

        self.registerObjects(self._qml_engine)

        Bindings.register()
        self._qml_engine.load(self._main_qml)
        self.engineCreatedSignal.emit()

    recentFilesChanged = pyqtSignal()

    @pyqtProperty("QVariantList", notify=recentFilesChanged)
    def recentFiles(self) -> List[QUrl]:
        return self._recent_files

    def _onJobFinished(self, job: Job) -> None:
        if isinstance(job, WriteFileJob):
            if not job.getResult() or not job.getAddToRecentFiles():
                # For a write file job, if it failed or it doesn't need to be added to the recent files list, we do not
                # add it.
                return
        elif (not isinstance(job, ReadMeshJob) and not isinstance(job, ReadFileJob)) or not job.getResult():
            return

        if isinstance(job, (ReadMeshJob, ReadFileJob, WriteFileJob)):
            self.addFileToRecentFiles(job.getFileName())

    def addFileToRecentFiles(self, file_name: str) -> None:
        file_path = QUrl.fromLocalFile(file_name)

        if file_path in self._recent_files:
            self._recent_files.remove(file_path)

        self._recent_files.insert(0, file_path)
        if len(self._recent_files) > 10:
            del self._recent_files[10]

        pref = ""
        for path in self._recent_files:
            pref += path.toLocalFile() + ";"

        self.getPreferences().setValue("%s/recent_files" % self.getApplicationName(), pref)
        self.recentFilesChanged.emit()

    def run(self) -> None:
        super().run()

    def hideMessage(self, message: Message) -> None:
        with self._message_lock:
            if message in self._visible_messages:
                message.hide(send_signal = False)  # we're in handling hideMessageSignal so we don't want to resend it
                self._visible_messages.remove(message)
                self.visibleMessageRemoved.emit(message)

    def showMessage(self, message: Message) -> None:
        with self._message_lock:
            if message not in self._visible_messages:
                self._visible_messages.append(message)
                message.setLifetimeTimer(QTimer())
                message.setInactivityTimer(QTimer())
                self.visibleMessageAdded.emit(message)

        # also show toast message when the main window is minimized
        self.showToastMessage(self._app_name, message.getText())

    def _onMainWindowStateChanged(self, window_state: int) -> None:
        if self._tray_icon and self._tray_icon_widget:
            visible = window_state == Qt.WindowMinimized
            self._tray_icon_widget.setVisible(visible)

    # Show toast message using System tray widget.
    def showToastMessage(self, title: str, message: str) -> None:
        if self.checkWindowMinimizedState() and self._tray_icon_widget:
            # NOTE: Qt 5.8 don't support custom icon for the system tray messages, but Qt 5.9 does.
            #       We should use the custom icon when we switch to Qt 5.9
            self._tray_icon_widget.showMessage(title, message)

    def setMainQml(self, path: str) -> None:
        self._main_qml = path

    def exec_(self, *args: Any, **kwargs: Any) -> None:
        self.applicationRunning.emit()
        super().exec_(*args, **kwargs)
        
    @pyqtSlot()
    def reloadQML(self) -> None:
        # only reload when it is a release build
        if not self.getIsDebugMode():
            return
        if self._qml_engine and self._theme:
            self._qml_engine.clearComponentCache()
            self._theme.reload()
            self._qml_engine.load(self._main_qml)
            # Hide the window. For some reason we can't close it yet. This needs to be done in the onComponentCompleted.
            for obj in self._qml_engine.rootObjects():
                if obj != self._qml_engine.rootObjects()[-1]:
                    obj.hide()

    @pyqtSlot()
    def purgeWindows(self) -> None:
        # Close all root objects except the last one.
        # Should only be called by onComponentCompleted of the mainWindow.
        if self._qml_engine:
            for obj in self._qml_engine.rootObjects():
                if obj != self._qml_engine.rootObjects()[-1]:
                    obj.close()

    @pyqtSlot("QList<QQmlError>")
    def __onQmlWarning(self, warnings: List[QQmlError]) -> None:
        for warning in warnings:
            Logger.log("w", warning.toString())

    engineCreatedSignal = Signal()

    def isShuttingDown(self) -> bool:
        return self._is_shutting_down

    def registerObjects(self, engine) -> None: #type: ignore #Don't type engine, because the type depends on the platform you're running on so it always gives an error somewhere.
        engine.rootContext().setContextProperty("PluginRegistry", PluginRegistry.getInstance())

    def getRenderer(self) -> QtRenderer:
        if not self._renderer:
            self._renderer = QtRenderer()

        return cast(QtRenderer, self._renderer)

    mainWindowChanged = Signal()

    def getMainWindow(self) -> Optional[MainWindow]:
        return self._main_window

    def setMainWindow(self, window: MainWindow) -> None:
        if window != self._main_window:
            if self._main_window is not None:
                self._main_window.windowStateChanged.disconnect(self._onMainWindowStateChanged)

            self._main_window = window
            if self._main_window is not None:
                self._main_window.windowStateChanged.connect(self._onMainWindowStateChanged)

            self.mainWindowChanged.emit()

    def setVisible(self, visible: bool) -> None:
        if self._main_window is not None:
            self._main_window.visible = visible

    @property
    def isVisible(self) -> bool:
        if self._main_window is not None:
            return self._main_window.visible #type: ignore #MyPy doesn't realise that self._main_window cannot be None here.
        return False

    def getTheme(self) -> Optional[Theme]:
        if self._theme is None:
            if self._qml_engine is None:
                Logger.log("e", "The theme cannot be accessed before the engine is initialised")
                return None

            self._theme = UM.Qt.Bindings.Theme.Theme.getInstance(self._qml_engine)
        return self._theme

    #   Handle a function that should be called later.
    def functionEvent(self, event: QEvent) -> None:
        e = _QtFunctionEvent(event)
        QCoreApplication.postEvent(self, e)

    #   Handle Qt events
    def event(self, event: QEvent) -> bool:
        if event.type() == _QtFunctionEvent.QtFunctionEvent:
            event._function_event.call()
            return True

        return super().event(event)

    def windowClosed(self, save_data: bool = True) -> None:
        Logger.log("d", "Shutting down %s", self.getApplicationName())
        self._is_shutting_down = True

        # garbage collect tray icon so it gets properly closed before the application is closed
        self._tray_icon_widget = None

        if save_data:
            try:
                self.savePreferences()
            except Exception as e:
                Logger.log("e", "Exception while saving preferences: %s", repr(e))

        try:
            self.applicationShuttingDown.emit()
        except Exception as e:
            Logger.log("e", "Exception while emitting shutdown signal: %s", repr(e))

        try:
            self.getBackend().close()
        except Exception as e:
            Logger.log("e", "Exception while closing backend: %s", repr(e))

        if self._tray_icon_widget:
            self._tray_icon_widget.deleteLater()

        self.quit()

    def checkWindowMinimizedState(self) -> bool:
        if self._main_window is not None and self._main_window.windowState() == Qt.WindowMinimized:
            return True
        else:
            return False

    ##  Get the backend of the application (the program that does the heavy lifting).
    #   The backend is also a QObject, which can be used from qml.
    @pyqtSlot(result = "QObject*")
    def getBackend(self) -> Backend:
        return self._backend

    ##  Property used to expose the backend
    #   It is made static as the backend is not supposed to change during runtime.
    #   This makes the connection between backend and QML more reliable than the pyqtSlot above.
    #   \returns Backend \type{Backend}
    @pyqtProperty("QVariant", constant = True)
    def backend(self) -> Backend:
        return self.getBackend()

    ## Create a class variable so we can manage the splash in the CrashHandler dialog when the Application instance
    # is not yet created, e.g. when an error occurs during the initialization
    splash = None  # type: Optional[QSplashScreen]

    def createSplash(self) -> None:
        if not self.getIsHeadLess():
            try:
                QtApplication.splash = self._createSplashScreen()
            except FileNotFoundError:
                QtApplication.splash = None
            else:
                if QtApplication.splash:
                    QtApplication.splash.show()
                    self.processEvents()

    ##  Display text on the splash screen.
    def showSplashMessage(self, message: str) -> None:
        if not QtApplication.splash:
            self.createSplash()
        
        if QtApplication.splash:
            QtApplication.splash.showMessage(message, Qt.AlignHCenter | Qt.AlignVCenter)
            self.processEvents()
        elif self.getIsHeadLess():
            Logger.log("d", message)

    ##  Close the splash screen after the application has started.
    def closeSplash(self) -> None:
        if QtApplication.splash:
            QtApplication.splash.close()
            QtApplication.splash = None

    ## Create a QML component from a qml file.
    #  \param qml_file_path: The absolute file path to the root qml file.
    #  \param context_properties: Optional dictionary containing the properties that will be set on the context of the
    #                              qml instance before creation.
    #  \return None in case the creation failed (qml error), else it returns the qml instance.
    #  \note If the creation fails, this function will ensure any errors are logged to the logging service.
    def createQmlComponent(self, qml_file_path: str, context_properties: Dict[str, "QObject"] = None) -> Optional["QObject"]:
        if self._qml_engine is None: # Protect in case the engine was not initialized yet
            return None
        path = QUrl.fromLocalFile(qml_file_path)
        component = QQmlComponent(self._qml_engine, path)
        result_context = QQmlContext(self._qml_engine.rootContext()) #type: ignore #MyPy doens't realise that self._qml_engine can't be None here.
        if context_properties is not None:
            for name, value in context_properties.items():
                result_context.setContextProperty(name, value)
        result = component.create(result_context)
        for err in component.errors():
            Logger.log("e", str(err.toString()))
        if result is None:
            return None

        # We need to store the context with the qml object, else the context gets garbage collected and the qml objects
        # no longer function correctly/application crashes.
        result.attached_context = result_context
        return result

    ##  Delete all nodes containing mesh data in the scene.
    #   \param only_selectable. Set this to False to delete objects from all build plates
    @pyqtSlot()
    def deleteAll(self, only_selectable = True) -> None:
        Logger.log("i", "Clearing scene")
        if not self.getController().getToolsEnabled():
            return

        nodes = []
        for node in DepthFirstIterator(self.getController().getScene().getRoot()): #type: ignore #Ignore type error because iter() should get called automatically by Python syntax.
            if not isinstance(node, SceneNode):
                continue
            if (not node.getMeshData() and not node.callDecoration("getLayerData")) and not node.callDecoration("isGroup"):
                continue  # Node that doesnt have a mesh and is not a group.
            if only_selectable and not node.isSelectable():
                continue
            if not node.callDecoration("isSliceable") and not node.callDecoration("getLayerData") and not node.callDecoration("isGroup"):
                continue  # Only remove nodes that are selectable.
            if node.getParent() and cast(SceneNode, node.getParent()).callDecoration("isGroup"):
                continue  # Grouped nodes don't need resetting as their parent (the group) is resetted)
            nodes.append(node)
        if nodes:
            op = GroupedOperation()

            for node in nodes:
                op.addOperation(RemoveSceneNodeOperation(node))

                # Reset the print information
                self.getController().getScene().sceneChanged.emit(node)

            op.push()
            Selection.clear()

    ##  Get the MeshFileHandler of this application.
    def getMeshFileHandler(self) -> MeshFileHandler:
        return self._mesh_file_handler

    def getWorkspaceFileHandler(self) -> WorkspaceFileHandler:
        return self._workspace_file_handler

    @pyqtSlot(result = QObject)
    def getPackageManager(self) -> PackageManager:
        return self._package_manager

    ##  Gets the instance of this application.
    #
    #   This is just to further specify the type of Application.getInstance().
    #   \return The instance of this application.
    @classmethod
    def getInstance(cls, *args, **kwargs) -> "QtApplication":
        return cast(QtApplication, super().getInstance(**kwargs))

    def _createSplashScreen(self) -> QSplashScreen:
        return QSplashScreen(QPixmap(Resources.getPath(Resources.Images, self.getApplicationName() + ".png")))

    def _screenScaleFactor(self) -> float:
        # OSX handles sizes of dialogs behind our backs, but other platforms need
        # to know about the device pixel ratio
        if sys.platform == "darwin":
            return 1.0
        else:
            # determine a device pixel ratio from font metrics, using the same logic as UM.Theme
            fontPixelRatio = QFontMetrics(QCoreApplication.instance().font()).ascent() / 11
            # round the font pixel ratio to quarters
            fontPixelRatio = int(fontPixelRatio * 4) / 4
            return fontPixelRatio
Beispiel #6
0
class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.ld_services = ['LeaderTerm', 'LeaderTermDaemon']
        self.ld_process = [
            'LdFileGate.exe', 'LdTerm.exe', 'LdTermDaemon.exe',
            'LdTermPlug64.exe', 'LdTermPlug.exe', 'LdTermDaemon.exe',
            'LdApproval.exe'
        ]
        self.ld_process = [p.lower() for p in self.ld_process]

        QApplication.instance().aboutToQuit.connect(self.onApplicationQuit)
        self.createSystray()

    def createSystray(self):
        self.systray = QSystemTrayIcon(self)
        self.systray.show()
        self.systray.setToolTip('制作人:宝乐-质量控制部')

        trayMenu = QMenu()

        start_act = QAction('启动绿盾', triggered=self.startLd, parent=trayMenu)
        start_act.setIcon(self.style().standardIcon(QStyle.SP_MediaPlay))
        trayMenu.addAction(start_act)

        stop_act = QAction('停止绿盾', triggered=self.stopLd, parent=trayMenu)
        stop_act.setToolTip('一般情况下停止绿盾')
        stop_act.setIcon(self.style().standardIcon(QStyle.SP_MediaPause))
        trayMenu.addAction(stop_act)

        trayMenu.addSeparator()
        quit_act = QAction('退出', triggered=QApplication.quit, parent=trayMenu)
        quit_act.setIcon(self.style().standardIcon(
            QStyle.SP_DialogCloseButton))
        trayMenu.addAction(quit_act)
        self.systray.setContextMenu(trayMenu)

        pix = QPixmap()
        pix.loadFromData(icon_data, 'png')
        icon = QIcon(pix)
        self.systray.setIcon(icon)

    def closeEvent(self, event):
        event.ignore()

    def onApplicationQuit(self):
        self.systray.deleteLater()

    def stopLd(self):
        while True:
            try:
                self.stopLdProcess()
            except Exception:
                break

    def startLd(self):
        while True:
            try:
                self.startLdServices()
            except Exception as e:
                break

    def stopLdProcess(self):
        i = 0
        for process in psutil.process_iter():
            if process.name().lower() in self.ld_process:
                print(process.memory_info())
                process.kill()
                i += 1
        if i == 0:
            raise RuntimeError('No Process Found')

    def stopLdServices(self):
        for service in self.ld_services:
            win32serviceutil.StopService(service)

    def startLdServices(self):
        for service in self.ld_services:
            win32serviceutil.StartService(service)
Beispiel #7
0
class LoginDialog(QDialog):
    def __init__(self, steam, theme):
        super().__init__()
        self.steam = steam
        self.theme = theme
        self.trayicon = None
        self.wait_task = None
        self.process = None
        self._exit = False
        self._login = None
        self.user_widgets = []
        self.setLayout(QVBoxLayout())
        self.setWindowTitle("Steam Acolyte")
        self.setWindowIcon(theme.window_icon)
        self.setStyleSheet(theme.window_style)
        steam.command_received.connect(lambda *_: self.activateWindow())
        self.update_userlist()

    def update_userlist(self):
        """Update the user list widget from the config file."""
        self.clear_layout()
        users = sorted(self.steam.users(),
                       key=lambda u:
                       (u.persona_name.lower(), u.account_name.lower()))
        users.append(SteamUser('', '', '', ''))
        for user in users:
            self.layout().addWidget(UserWidget(self, user))

    def clear_layout(self):
        """Remove all users from the user list widget."""
        # The safest way I found to clear a QLayout is to reparent it to a
        # temporary widget. This also recursively reparents, hides and later
        # destroys any child widgets.
        layout = self.layout()
        if layout is not None:
            dump = QWidget()
            dump.setLayout(layout)
            dump.deleteLater()
        self.setLayout(QVBoxLayout())

    @trace.method
    def wait_for_lock(self):
        """Start waiting for the steam instance lock asynchronously, and
        show/activate the window when we acquire the lock."""
        if self._exit:
            self.close()
            return
        self.wait_task = AsyncTask(self.steam.wait_for_lock)
        self.wait_task.finished.connect(self._on_locked)
        self.wait_task.start()

    @trace.method
    def _on_locked(self):
        """Executed when steam instance lock is acquired. Executes any queued
        login command, or activates the user list widget if no command was
        queued."""
        if self._exit:
            self.close()
            return
        self.stopAction.setEnabled(False)
        self.wait_task = None
        self.steam.store_login_cookie()
        self.update_userlist()
        if self._login:
            self.run_steam(self._login)
            self._login = None
            return
        self.show()

    @trace.method
    def show_trayicon(self):
        """Create and show the tray icon."""
        # The conversion to QPixmap and back to QIcon is needed to prevent a
        # bug that leads to the icon not being displayed in plasma. See:
        # - https://github.com/coldfix/steam-acolyte/issues/8
        # - https://bugreports.qt.io/browse/QTBUG-53550
        icon = QIcon(self.theme.window_icon.pixmap(64))
        self.trayicon = QSystemTrayIcon(icon)
        self.trayicon.setVisible(True)
        self.trayicon.setToolTip("acolyte - lightweight steam account manager")
        self.trayicon.activated.connect(self.trayicon_clicked)
        self.trayicon.setContextMenu(self.createMenu())

    @trace.method
    def hide_trayicon(self):
        """Hide and destroy the tray icon."""
        if self.trayicon is not None:
            self.trayicon.setVisible(False)
            self.trayicon.deleteLater()
            self.trayicon = None

    @trace.method
    def trayicon_clicked(self, reason):
        """Activate window when tray icon is left-clicked."""
        if reason == QSystemTrayIcon.Trigger:
            if self.steam.has_steam_lock():
                self.activateWindow()

    def createMenu(self):
        """Compose tray menu."""
        style = self.style()
        stop = self.stopAction = QAction('&Exit Steam', self)
        stop.setToolTip('Signal steam to exit.')
        stop.setIcon(style.standardIcon(QStyle.SP_MediaStop))
        stop.triggered.connect(self.exit_steam)
        stop.setEnabled(False)
        exit = QAction('&Quit', self)
        exit.setToolTip('Exit acolyte.')
        exit.setIcon(style.standardIcon(QStyle.SP_DialogCloseButton))
        exit.triggered.connect(self._on_exit, QueuedConnection)

        self.newUserAction = make_user_action(self, SteamUser('', '', '', ''))
        self.userActions = []
        menu = QMenu()
        menu.addSection('Login')
        menu.addAction(self.newUserAction)
        menu.addSeparator()
        menu.addAction(stop)
        menu.addAction(exit)
        menu.aboutToShow.connect(self.update_menu, QueuedConnection)
        return menu

    def update_menu(self):
        """Update menu just before showing: populate with current user list
        and set position from tray icon."""
        self.populate_menu()
        self.position_menu()

    def populate_menu(self):
        """Update user list menuitems in tray menu."""
        menu = self.trayicon.contextMenu()
        for action in self.userActions:
            menu.removeAction(action)
        users = sorted(self.steam.users(),
                       key=lambda u:
                       (u.persona_name.lower(), u.account_name.lower()))
        self.userActions = [make_user_action(self, user) for user in users]
        menu.insertActions(self.newUserAction, self.userActions)

    def position_menu(self):
        """Set menu position from tray icon."""
        menu = self.trayicon.contextMenu()
        desktop = QApplication.desktop()
        screen = QApplication.screens()[desktop.screenNumber(menu)]

        screen_geom = screen.availableGeometry()
        menu_size = menu.sizeHint()
        icon_geom = self.trayicon.geometry()

        if icon_geom.left() + menu_size.width() <= screen_geom.right():
            left = icon_geom.left()
        elif icon_geom.right() - menu_size.width() >= screen_geom.left():
            left = icon_geom.right() - menu_size.width()
        else:
            return

        if icon_geom.bottom() + menu_size.height() <= screen_geom.bottom():
            top = icon_geom.bottom()
        elif icon_geom.top() - menu_size.height() >= screen_geom.top():
            top = icon_geom.top() - menu_size.height()
        else:
            return

        menu.move(left, top)

    @trace.method
    def exit_steam(self):
        """Send shutdown command to steam."""
        self.stopAction.setEnabled(False)
        self.steam.stop()

    @trace.method
    def _on_exit(self):
        """Exit acolyte."""
        # We can't quit if steam is still running because QProcess would
        # terminate the child with us. In this case, we hide the trayicon and
        # set an exit flag to remind us about to exit as soon as steam is
        # finished.
        self.hide_trayicon()
        if self.steam.has_steam_lock():
            self.close()
        else:
            self._exit = True
            self.steam.unlock()
            self.steam.release_acolyte_instance_lock()

    @trace.method
    def login(self, username):
        """
        Exit steam if open, and login the user with the given username.
        """
        if self.steam.has_steam_lock():
            self.run_steam(username)
        else:
            self._login = username
            self.exit_steam()

    @trace.method
    def run_steam(self, username):
        """Run steam as the given user."""
        # Close and recreate after steam is finished. This serves two purposes:
        # 1. update user list and widget state
        # 2. fix ":hover" selector not working on linux after hide+show
        self.hide()
        self.steam.switch_user(username)
        self.steam.unlock()
        self.stopAction.setEnabled(True)
        self.process = self.steam.run()
        self.process.finished.connect(self.wait_for_lock)

    @trace.method
    def show_waiting_message(self):
        """If we are in the background, show waiting message as balloon."""
        if self.trayicon is not None:
            self.trayicon.showMessage("steam-acolyte",
                                      "The damned stand ready.")
Beispiel #8
0
class TriblerWindow(QMainWindow):

    resize_event = pyqtSignal()
    escape_pressed = pyqtSignal()
    received_search_completions = pyqtSignal(object)

    def on_exception(self, *exc_info):
        if self.tray_icon:
            self.tray_icon.deleteLater()

        # Stop the download loop
        self.downloads_page.stop_loading_downloads()

        # Add info about whether we are stopping Tribler or not
        os.environ['TRIBLER_SHUTTING_DOWN'] = str(self.core_manager.shutting_down)

        if not self.core_manager.shutting_down:
            self.core_manager.stop(stop_app_on_shutdown=False)

        self.setHidden(True)

        if self.debug_window:
            self.debug_window.setHidden(True)

        exception_text = "".join(traceback.format_exception(*exc_info))
        logging.error(exception_text)

        if not self.feedback_dialog_is_open:
            dialog = FeedbackDialog(self, exception_text, self.core_manager.events_manager.tribler_version,
                                    self.start_time)
            self.feedback_dialog_is_open = True
            _ = dialog.exec_()

    def __init__(self):
        QMainWindow.__init__(self)

        self.navigation_stack = []
        self.feedback_dialog_is_open = False
        self.tribler_started = False
        self.tribler_settings = None
        self.debug_window = None
        self.core_manager = CoreManager()
        self.pending_requests = {}
        self.pending_uri_requests = []
        self.download_uri = None
        self.dialog = None
        self.start_download_dialog_active = False
        self.request_mgr = None
        self.search_request_mgr = None
        self.search_suggestion_mgr = None
        self.selected_torrent_files = []
        self.vlc_available = True
        self.has_search_results = False
        self.start_time = time.time()

        sys.excepthook = self.on_exception

        uic.loadUi(get_ui_file_path('mainwindow.ui'), self)
        TriblerRequestManager.window = self
        self.tribler_status_bar.hide()

        self.magnet_handler = MagnetHandler(self.window)
        QDesktopServices.setUrlHandler("magnet", self.magnet_handler, "on_open_magnet_link")

        QCoreApplication.setOrganizationDomain("nl")
        QCoreApplication.setOrganizationName("TUDelft")
        QCoreApplication.setApplicationName("Tribler")
        QCoreApplication.setAttribute(Qt.AA_UseHighDpiPixmaps)

        self.read_settings()

        # Remove the focus rect on OS X
        for widget in self.findChildren(QLineEdit) + self.findChildren(QListWidget) + self.findChildren(QTreeWidget):
            widget.setAttribute(Qt.WA_MacShowFocusRect, 0)

        self.menu_buttons = [self.left_menu_button_home, self.left_menu_button_search, self.left_menu_button_my_channel,
                             self.left_menu_button_subscriptions, self.left_menu_button_video_player,
                             self.left_menu_button_downloads, self.left_menu_button_discovered]

        self.video_player_page.initialize_player()
        self.search_results_page.initialize_search_results_page()
        self.settings_page.initialize_settings_page()
        self.subscribed_channels_page.initialize()
        self.edit_channel_page.initialize_edit_channel_page()
        self.downloads_page.initialize_downloads_page()
        self.home_page.initialize_home_page()
        self.loading_page.initialize_loading_page()
        self.discovering_page.initialize_discovering_page()
        self.discovered_page.initialize_discovered_page()
        self.trust_page.initialize_trust_page()

        self.stackedWidget.setCurrentIndex(PAGE_LOADING)

        # Create the system tray icon
        if QSystemTrayIcon.isSystemTrayAvailable():
            self.tray_icon = QSystemTrayIcon()
            use_monochrome_icon = get_gui_setting(self.gui_settings, "use_monochrome_icon", False, is_bool=True)
            self.update_tray_icon(use_monochrome_icon)
        else:
            self.tray_icon = None

        self.hide_left_menu_playlist()
        self.left_menu_button_debug.setHidden(True)
        self.top_menu_button.setHidden(True)
        self.left_menu.setHidden(True)
        self.trust_button.setHidden(True)
        self.settings_button.setHidden(True)
        self.add_torrent_button.setHidden(True)
        self.top_search_bar.setHidden(True)

        # Set various icons
        self.top_menu_button.setIcon(QIcon(get_image_path('menu.png')))

        self.search_completion_model = QStringListModel()
        completer = QCompleter()
        completer.setModel(self.search_completion_model)
        completer.setCompletionMode(QCompleter.UnfilteredPopupCompletion)
        self.item_delegate = QStyledItemDelegate()
        completer.popup().setItemDelegate(self.item_delegate)
        completer.popup().setStyleSheet("""
        QListView {
            background-color: #404040;
        }

        QListView::item {
            color: #D0D0D0;
            padding-top: 5px;
            padding-bottom: 5px;
        }

        QListView::item:hover {
            background-color: #707070;
        }
        """)
        self.top_search_bar.setCompleter(completer)

        # Toggle debug if developer mode is enabled
        self.window().left_menu_button_debug.setHidden(
            not get_gui_setting(self.gui_settings, "debug", False, is_bool=True))

        self.core_manager.start()

        self.core_manager.events_manager.received_search_result_channel.connect(
            self.search_results_page.received_search_result_channel)
        self.core_manager.events_manager.received_search_result_torrent.connect(
            self.search_results_page.received_search_result_torrent)
        self.core_manager.events_manager.torrent_finished.connect(self.on_torrent_finished)
        self.core_manager.events_manager.new_version_available.connect(self.on_new_version_available)
        self.core_manager.events_manager.tribler_started.connect(self.on_tribler_started)

        # Install signal handler for ctrl+c events
        def sigint_handler(*_):
            self.close_tribler()

        signal.signal(signal.SIGINT, sigint_handler)

        self.installEventFilter(self.video_player_page)

        self.show()

    def update_tray_icon(self, use_monochrome_icon):
        if not QSystemTrayIcon.isSystemTrayAvailable():
            return

        if use_monochrome_icon:
            self.tray_icon.setIcon(QIcon(QPixmap(get_image_path('monochrome_tribler.png'))))
        else:
            self.tray_icon.setIcon(QIcon(QPixmap(get_image_path('tribler.png'))))
        self.tray_icon.show()

    def on_torrent_finished(self, torrent_info):
        if self.tray_icon:
            self.window().tray_icon.showMessage("Download finished",
                                                "Download of %s has finished." % torrent_info["name"])

    def show_loading_screen(self):
        self.top_menu_button.setHidden(True)
        self.left_menu.setHidden(True)
        self.trust_button.setHidden(True)
        self.settings_button.setHidden(True)
        self.add_torrent_button.setHidden(True)
        self.top_search_bar.setHidden(True)
        self.stackedWidget.setCurrentIndex(PAGE_LOADING)

    def on_tribler_started(self):
        self.tribler_started = True

        self.top_menu_button.setHidden(False)
        self.left_menu.setHidden(False)
        self.trust_button.setHidden(False)
        self.settings_button.setHidden(False)
        self.add_torrent_button.setHidden(False)
        self.top_search_bar.setHidden(False)

        # fetch the settings, needed for the video player port
        self.request_mgr = TriblerRequestManager()
        self.fetch_settings()

        self.downloads_page.start_loading_downloads()
        self.home_page.load_popular_torrents()
        if not self.gui_settings.value("first_discover", False) and not self.core_manager.use_existing_core:
            self.window().gui_settings.setValue("first_discover", True)
            self.discovering_page.is_discovering = True
            self.stackedWidget.setCurrentIndex(PAGE_DISCOVERING)
        else:
            self.clicked_menu_button_home()

    def show_status_bar(self, message):
        self.tribler_status_bar_label.setText(message)
        self.tribler_status_bar.show()

    def hide_status_bar(self):
        self.tribler_status_bar.hide()

    def process_uri_request(self):
        """
        Process a URI request if we have one in the queue.
        """
        if len(self.pending_uri_requests) == 0:
            return

        uri = self.pending_uri_requests.pop()
        if uri.startswith('file') or uri.startswith('magnet'):
            self.start_download_from_uri(uri)

    def perform_start_download_request(self, uri, anon_download, safe_seeding, destination, selected_files,
                                       total_files=0, callback=None):
        selected_files_uri = ""
        if len(selected_files) != total_files:  # Not all files included
            selected_files_uri = u'&' + u''.join(u"selected_files[]=%s&" % file for file in selected_files)[:-1]

        anon_hops = int(self.tribler_settings['download_defaults']['number_hops']) if anon_download else 0
        safe_seeding = 1 if safe_seeding else 0
        post_data = "uri=%s&anon_hops=%d&safe_seeding=%d&destination=%s%s" % (uri, anon_hops, safe_seeding,
                                                                                   destination, selected_files_uri)
        post_data = post_data.encode('utf-8')  # We need to send bytes in the request, not unicode

        request_mgr = TriblerRequestManager()
        self.pending_requests[request_mgr.request_id] = request_mgr
        request_mgr.perform_request("downloads", callback if callback else self.on_download_added,
                                    method='PUT', data=post_data)

        # Save the download location to the GUI settings
        current_settings = get_gui_setting(self.gui_settings, "recent_download_locations", "")
        recent_locations = current_settings.split(",") if len(current_settings) > 0 else []
        encoded_destination = destination.encode('hex')
        if encoded_destination in recent_locations:
            recent_locations.remove(encoded_destination)
        recent_locations.insert(0, encoded_destination)

        if len(recent_locations) > 5:
            recent_locations = recent_locations[:5]

        self.gui_settings.setValue("recent_download_locations", ','.join(recent_locations))

    def on_new_version_available(self, version):
        if version == str(self.gui_settings.value('last_reported_version')):
            return

        self.dialog = ConfirmationDialog(self, "New version available",
                                         "Version %s of Tribler is available.Do you want to visit the website to "
                                         "download the newest version?" % version,
                                         [('IGNORE', BUTTON_TYPE_NORMAL), ('LATER', BUTTON_TYPE_NORMAL),
                                          ('OK', BUTTON_TYPE_NORMAL)])
        self.dialog.button_clicked.connect(lambda action: self.on_new_version_dialog_done(version, action))
        self.dialog.show()

    def on_new_version_dialog_done(self, version, action):
        if action == 0:  # ignore
            self.gui_settings.setValue("last_reported_version", version)
        elif action == 2:  # ok
            import webbrowser
            webbrowser.open("https://tribler.org")

        self.dialog.setParent(None)
        self.dialog = None

    def read_settings(self):
        self.gui_settings = QSettings()
        center = QApplication.desktop().availableGeometry(self).center()
        pos = self.gui_settings.value("pos", QPoint(center.x() - self.width() * 0.5, center.y() - self.height() * 0.5))
        size = self.gui_settings.value("size", self.size())

        self.move(pos)
        self.resize(size)

    def on_search_text_change(self, text):
        self.search_suggestion_mgr = TriblerRequestManager()
        self.search_suggestion_mgr.perform_request(
            "search/completions?q=%s" % text, self.on_received_search_completions)

    def on_received_search_completions(self, completions):
        self.received_search_completions.emit(completions)
        self.search_completion_model.setStringList(completions["completions"])

    def fetch_settings(self):
        self.request_mgr = TriblerRequestManager()
        self.request_mgr.perform_request("settings", self.received_settings, capture_errors=False)

    def received_settings(self, settings):
        # If we cannot receive the settings, stop Tribler with an option to send the crash report.
        if 'error' in settings:
            raise RuntimeError(TriblerRequestManager.get_message_from_error(settings))
        else:
            self.tribler_settings = settings['settings']

        # Disable various components based on the settings
        if not self.tribler_settings['search_community']['enabled']:
            self.window().top_search_bar.setHidden(True)
        if not self.tribler_settings['video_server']['enabled']:
            self.left_menu_button_video_player.setHidden(True)

        # Set the video server port
        self.video_player_page.video_player_port = self.tribler_settings["video_server"]["port"]

        # process pending file requests (i.e. someone clicked a torrent file when Tribler was closed)
        # We do this after receiving the settings so we have the default download location.
        self.process_uri_request()

    def on_top_search_button_click(self):
        self.left_menu_button_search.setChecked(True)
        self.has_search_results = True
        self.clicked_menu_button_search()
        self.search_results_page.perform_search(self.top_search_bar.text())
        self.search_request_mgr = TriblerRequestManager()
        self.search_request_mgr.perform_request("search?q=%s" % self.top_search_bar.text(), None)

    def on_settings_button_click(self):
        self.deselect_all_menu_buttons()
        self.stackedWidget.setCurrentIndex(PAGE_SETTINGS)
        self.settings_page.load_settings()
        self.navigation_stack = []
        self.hide_left_menu_playlist()

    def on_trust_button_click(self):
        self.deselect_all_menu_buttons()
        self.stackedWidget.setCurrentIndex(PAGE_TRUST)
        self.trust_page.load_trust_statistics()
        self.navigation_stack = []
        self.hide_left_menu_playlist()

    def on_add_torrent_button_click(self, pos):
        menu = TriblerActionMenu(self)

        browse_files_action = QAction('Import torrent from file', self)
        browse_directory_action = QAction('Import torrents from directory', self)
        add_url_action = QAction('Import torrent from magnet/URL', self)

        browse_files_action.triggered.connect(self.on_add_torrent_browse_file)
        browse_directory_action.triggered.connect(self.on_add_torrent_browse_dir)
        add_url_action.triggered.connect(self.on_add_torrent_from_url)

        menu.addAction(browse_files_action)
        menu.addAction(browse_directory_action)
        menu.addAction(add_url_action)

        menu.exec_(self.mapToGlobal(self.add_torrent_button.pos()))

    def on_add_torrent_browse_file(self):
        filenames = QFileDialog.getOpenFileNames(self,
                                                 "Please select the .torrent file", "", "Torrent files (*.torrent)")
        if len(filenames[0]) > 0:
            [self.pending_uri_requests.append(u"file:%s" % filename) for filename in filenames[0]]
            self.process_uri_request()

    def start_download_from_uri(self, uri):
        self.download_uri = uri

        if get_gui_setting(self.gui_settings, "ask_download_settings", True, is_bool=True):
            self.dialog = StartDownloadDialog(self.window().stackedWidget, self.download_uri)
            self.dialog.button_clicked.connect(self.on_start_download_action)
            self.dialog.show()
            self.start_download_dialog_active = True
        else:
            self.window().perform_start_download_request(self.download_uri,
                                                         self.window().tribler_settings['download_defaults'][
                                                             'anonymity_enabled'],
                                                         self.window().tribler_settings['download_defaults'][
                                                             'safeseeding_enabled'],
                                                         self.tribler_settings['download_defaults']['saveas'], [], 0)
            self.process_uri_request()

    def on_start_download_action(self, action):
        if action == 1:
            self.window().perform_start_download_request(self.download_uri,
                                                         self.dialog.dialog_widget.anon_download_checkbox.isChecked(),
                                                         self.dialog.dialog_widget.safe_seed_checkbox.isChecked(),
                                                         self.dialog.dialog_widget.destination_input.currentText(),
                                                         self.dialog.get_selected_files(),
                                                         self.dialog.dialog_widget.files_list_view.topLevelItemCount())

        self.dialog.request_mgr.cancel_request()  # To abort the torrent info request
        self.dialog.setParent(None)
        self.dialog = None
        self.start_download_dialog_active = False

        if action == 0:  # We do this after removing the dialog since process_uri_request is blocking
            self.process_uri_request()

    def on_add_torrent_browse_dir(self):
        chosen_dir = QFileDialog.getExistingDirectory(self, "Please select the directory containing the .torrent files",
                                                      "", QFileDialog.ShowDirsOnly)

        if len(chosen_dir) != 0:
            self.selected_torrent_files = [torrent_file for torrent_file in glob.glob(chosen_dir + "/*.torrent")]
            self.dialog = ConfirmationDialog(self, "Add torrents from directory",
                                             "Are you sure you want to add %d torrents to Tribler?" %
                                             len(self.selected_torrent_files),
                                             [('ADD', BUTTON_TYPE_NORMAL), ('CANCEL', BUTTON_TYPE_CONFIRM)])
            self.dialog.button_clicked.connect(self.on_confirm_add_directory_dialog)
            self.dialog.show()

    def on_confirm_add_directory_dialog(self, action):
        if action == 0:
            for torrent_file in self.selected_torrent_files:
                escaped_uri = quote_plus((u"file:%s" % torrent_file).encode('utf-8'))
                self.perform_start_download_request(escaped_uri,
                                                    self.window().tribler_settings['download_defaults'][
                                                         'anonymity_enabled'],
                                                    self.window().tribler_settings['download_defaults'][
                                                         'safeseeding_enabled'],
                                                    self.tribler_settings['download_defaults']['saveas'], [], 0)

        self.dialog.setParent(None)
        self.dialog = None

    def on_add_torrent_from_url(self):
        self.dialog = ConfirmationDialog(self, "Add torrent from URL/magnet link",
                                         "Please enter the URL/magnet link in the field below:",
                                         [('ADD', BUTTON_TYPE_NORMAL), ('CANCEL', BUTTON_TYPE_CONFIRM)],
                                         show_input=True)
        self.dialog.dialog_widget.dialog_input.setPlaceholderText('URL/magnet link')
        self.dialog.dialog_widget.dialog_input.setFocus()
        self.dialog.button_clicked.connect(self.on_torrent_from_url_dialog_done)
        self.dialog.show()

    def on_torrent_from_url_dialog_done(self, action):
        uri = self.dialog.dialog_widget.dialog_input.text()

        # Remove first dialog
        self.dialog.setParent(None)
        self.dialog = None

        if action == 0:
            self.start_download_from_uri(uri)

    def on_download_added(self, result):
        if len(self.pending_uri_requests) == 0:  # Otherwise, we first process the remaining requests.
            self.window().left_menu_button_downloads.click()
        else:
            self.process_uri_request()

    def on_top_menu_button_click(self):
        if self.left_menu.isHidden():
            self.left_menu.show()
        else:
            self.left_menu.hide()

    def deselect_all_menu_buttons(self, except_select=None):
        for button in self.menu_buttons:
            if button == except_select:
                button.setEnabled(False)
                continue
            button.setEnabled(True)

            if button == self.left_menu_button_search and not self.has_search_results:
                button.setEnabled(False)

            button.setChecked(False)

    def clicked_menu_button_home(self):
        self.deselect_all_menu_buttons(self.left_menu_button_home)
        self.stackedWidget.setCurrentIndex(PAGE_HOME)
        self.navigation_stack = []
        self.hide_left_menu_playlist()

    def clicked_menu_button_search(self):
        self.deselect_all_menu_buttons(self.left_menu_button_search)
        self.stackedWidget.setCurrentIndex(PAGE_SEARCH_RESULTS)
        self.navigation_stack = []
        self.hide_left_menu_playlist()

    def clicked_menu_button_discovered(self):
        self.deselect_all_menu_buttons(self.left_menu_button_discovered)
        self.stackedWidget.setCurrentIndex(PAGE_DISCOVERED)
        self.discovered_page.load_discovered_channels()
        self.navigation_stack = []
        self.hide_left_menu_playlist()

    def clicked_menu_button_my_channel(self):
        self.deselect_all_menu_buttons(self.left_menu_button_my_channel)
        self.stackedWidget.setCurrentIndex(PAGE_EDIT_CHANNEL)
        self.edit_channel_page.load_my_channel_overview()
        self.navigation_stack = []
        self.hide_left_menu_playlist()

    def clicked_menu_button_video_player(self):
        self.deselect_all_menu_buttons(self.left_menu_button_video_player)
        self.stackedWidget.setCurrentIndex(PAGE_VIDEO_PLAYER)
        self.navigation_stack = []
        self.show_left_menu_playlist()

    def clicked_menu_button_downloads(self):
        self.deselect_all_menu_buttons(self.left_menu_button_downloads)
        self.stackedWidget.setCurrentIndex(PAGE_DOWNLOADS)
        self.navigation_stack = []
        self.hide_left_menu_playlist()

    def clicked_menu_button_debug(self):
        self.debug_window = DebugWindow(self.tribler_settings)
        self.debug_window.show()

    def clicked_menu_button_subscriptions(self):
        self.deselect_all_menu_buttons(self.left_menu_button_subscriptions)
        self.subscribed_channels_page.load_subscribed_channels()
        self.stackedWidget.setCurrentIndex(PAGE_SUBSCRIBED_CHANNELS)
        self.navigation_stack = []
        self.hide_left_menu_playlist()

    def hide_left_menu_playlist(self):
        self.left_menu_seperator.setHidden(True)
        self.left_menu_playlist_label.setHidden(True)
        self.left_menu_playlist.setHidden(True)

    def show_left_menu_playlist(self):
        self.left_menu_seperator.setHidden(False)
        self.left_menu_playlist_label.setHidden(False)
        self.left_menu_playlist.setHidden(False)

    def on_channel_item_click(self, channel_list_item):
        list_widget = channel_list_item.listWidget()
        from TriblerGUI.widgets.channel_list_item import ChannelListItem
        if isinstance(list_widget.itemWidget(channel_list_item), ChannelListItem):
            channel_info = channel_list_item.data(Qt.UserRole)
            self.channel_page.initialize_with_channel(channel_info)
            self.navigation_stack.append(self.stackedWidget.currentIndex())
            self.stackedWidget.setCurrentIndex(PAGE_CHANNEL_DETAILS)

    def on_playlist_item_click(self, playlist_list_item):
        list_widget = playlist_list_item.listWidget()
        from TriblerGUI.widgets.playlist_list_item import PlaylistListItem
        if isinstance(list_widget.itemWidget(playlist_list_item), PlaylistListItem):
            playlist_info = playlist_list_item.data(Qt.UserRole)
            self.playlist_page.initialize_with_playlist(playlist_info)
            self.navigation_stack.append(self.stackedWidget.currentIndex())
            self.stackedWidget.setCurrentIndex(PAGE_PLAYLIST_DETAILS)

    def on_page_back_clicked(self):
        try:
            prev_page = self.navigation_stack.pop()
            self.stackedWidget.setCurrentIndex(prev_page)
            if prev_page == PAGE_SEARCH_RESULTS:
                self.stackedWidget.widget(prev_page).load_search_results_in_list()
            if prev_page == PAGE_SUBSCRIBED_CHANNELS:
                self.stackedWidget.widget(prev_page).load_subscribed_channels()
            if prev_page == PAGE_DISCOVERED:
                self.stackedWidget.widget(prev_page).load_discovered_channels()
        except IndexError:
            logging.exception("Unknown page found in stack")

    def on_edit_channel_clicked(self):
        self.stackedWidget.setCurrentIndex(PAGE_EDIT_CHANNEL)
        self.navigation_stack = []
        self.channel_page.on_edit_channel_clicked()

    def resizeEvent(self, _):
        # Resize home page cells
        cell_width = self.home_page_table_view.width() / 3 - 3  # We have some padding to the right
        cell_height = cell_width / 2 + 60

        for i in range(0, 3):
            self.home_page_table_view.setColumnWidth(i, cell_width)
            self.home_page_table_view.setRowHeight(i, cell_height)
        self.resize_event.emit()

    def exit_full_screen(self):
        self.top_bar.show()
        self.left_menu.show()
        self.video_player_page.is_full_screen = False
        self.showNormal()

    def close_tribler(self):
        if not self.core_manager.shutting_down:
            if self.tray_icon:
                self.tray_icon.deleteLater()
            self.show_loading_screen()

            self.gui_settings.setValue("pos", self.pos())
            self.gui_settings.setValue("size", self.size())

            if self.core_manager.use_existing_core:
                # Don't close the core that we are using
                QApplication.quit()

            self.core_manager.stop()
            self.core_manager.shutting_down = True
            self.downloads_page.stop_loading_downloads()

    def closeEvent(self, close_event):
        self.close_tribler()
        close_event.ignore()

    def keyReleaseEvent(self, event):
        if event.key() == Qt.Key_Escape:
            self.escape_pressed.emit()
            if self.isFullScreen():
                self.exit_full_screen()
Beispiel #9
0
class ElectrumGui(BaseElectrumGui, Logger):

    network_dialog: Optional['NetworkDialog']
    lightning_dialog: Optional['LightningDialog']
    watchtower_dialog: Optional['WatchtowerDialog']

    @profiler
    def __init__(self, *, config: 'SimpleConfig', daemon: 'Daemon',
                 plugins: 'Plugins'):
        set_language(config.get('language', get_default_language()))
        BaseElectrumGui.__init__(self,
                                 config=config,
                                 daemon=daemon,
                                 plugins=plugins)
        Logger.__init__(self)
        self.logger.info(
            f"Qt GUI starting up... Qt={QtCore.QT_VERSION_STR}, PyQt={QtCore.PYQT_VERSION_STR}"
        )
        # Uncomment this call to verify objects are being properly
        # GC-ed when windows are closed
        #network.add_jobs([DebugMem([Abstract_Wallet, SPV, Synchronizer,
        #                            ElectrumWindow], interval=5)])
        QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_X11InitThreads)
        if hasattr(QtCore.Qt, "AA_ShareOpenGLContexts"):
            QtCore.QCoreApplication.setAttribute(
                QtCore.Qt.AA_ShareOpenGLContexts)
        if hasattr(QGuiApplication, 'setDesktopFileName'):
            QGuiApplication.setDesktopFileName('electrum-mona.desktop')
        self.gui_thread = threading.current_thread()
        self.windows = []  # type: List[ElectrumWindow]
        self.efilter = OpenFileEventFilter(self.windows)
        self.app = QElectrumApplication(sys.argv)
        self.app.installEventFilter(self.efilter)
        self.app.setWindowIcon(read_QIcon("electrum.png"))
        self._cleaned_up = False
        # timer
        self.timer = QTimer(self.app)
        self.timer.setSingleShot(False)
        self.timer.setInterval(500)  # msec

        self.network_dialog = None
        self.lightning_dialog = None
        self.watchtower_dialog = None
        self.network_updated_signal_obj = QNetworkUpdatedSignalObject()
        self._num_wizards_in_progress = 0
        self._num_wizards_lock = threading.Lock()
        self.dark_icon = self.config.get("dark_icon", False)
        self.tray = None
        self._init_tray()
        self.app.new_window_signal.connect(self.start_new_window)
        self.app.quit_signal.connect(self.app.quit, Qt.QueuedConnection)
        # maybe set dark theme
        self._default_qtstylesheet = self.app.styleSheet()
        self.reload_app_stylesheet()

        run_hook('init_qt', self)

    def _init_tray(self):
        self.tray = QSystemTrayIcon(self.tray_icon(), None)
        self.tray.setToolTip('Electrum')
        self.tray.activated.connect(self.tray_activated)
        self.build_tray_menu()
        self.tray.show()

    def reload_app_stylesheet(self):
        """Set the Qt stylesheet and custom colors according to the user-selected
        light/dark theme.
        TODO this can ~almost be used to change the theme at runtime (without app restart),
             except for util.ColorScheme... widgets already created with colors set using
             ColorSchemeItem.as_stylesheet() and similar will not get recolored.
             See e.g.
             - in Coins tab, the color for "frozen" UTXOs, or
             - in TxDialog, the receiving/change address colors
        """
        use_dark_theme = self.config.get('qt_gui_color_theme',
                                         'default') == 'dark'
        if use_dark_theme:
            try:
                import qdarkstyle
                self.app.setStyleSheet(qdarkstyle.load_stylesheet_pyqt5())
            except BaseException as e:
                use_dark_theme = False
                self.logger.warning(f'Error setting dark theme: {repr(e)}')
        else:
            self.app.setStyleSheet(self._default_qtstylesheet)
        # Apply any necessary stylesheet patches
        patch_qt_stylesheet(use_dark_theme=use_dark_theme)
        # Even if we ourselves don't set the dark theme,
        # the OS/window manager/etc might set *a dark theme*.
        # Hence, try to choose colors accordingly:
        ColorScheme.update_from_widget(QWidget(), force_dark=use_dark_theme)

    def build_tray_menu(self):
        if not self.tray:
            return
        # Avoid immediate GC of old menu when window closed via its action
        if self.tray.contextMenu() is None:
            m = QMenu()
            self.tray.setContextMenu(m)
        else:
            m = self.tray.contextMenu()
            m.clear()
        network = self.daemon.network
        m.addAction(_("Network"), self.show_network_dialog)
        if network and network.lngossip:
            m.addAction(_("Lightning Network"), self.show_lightning_dialog)
        if network and network.local_watchtower:
            m.addAction(_("Local Watchtower"), self.show_watchtower_dialog)
        for window in self.windows:
            name = window.wallet.basename()
            submenu = m.addMenu(name)
            submenu.addAction(_("Show/Hide"), window.show_or_hide)
            submenu.addAction(_("Close"), window.close)
        m.addAction(_("Dark/Light"), self.toggle_tray_icon)
        m.addSeparator()
        m.addAction(_("Exit Electrum"), self.app.quit)

    def tray_icon(self):
        if self.dark_icon:
            return read_QIcon('electrum_dark_icon.png')
        else:
            return read_QIcon('electrum_light_icon.png')

    def toggle_tray_icon(self):
        if not self.tray:
            return
        self.dark_icon = not self.dark_icon
        self.config.set_key("dark_icon", self.dark_icon, True)
        self.tray.setIcon(self.tray_icon())

    def tray_activated(self, reason):
        if reason == QSystemTrayIcon.DoubleClick:
            if all([w.is_hidden() for w in self.windows]):
                for w in self.windows:
                    w.bring_to_top()
            else:
                for w in self.windows:
                    w.hide()

    def _cleanup_before_exit(self):
        if self._cleaned_up:
            return
        self._cleaned_up = True
        self.app.new_window_signal.disconnect()
        self.efilter = None
        # If there are still some open windows, try to clean them up.
        for window in list(self.windows):
            window.close()
            window.clean_up()
        if self.network_dialog:
            self.network_dialog.close()
            self.network_dialog.clean_up()
            self.network_dialog = None
        self.network_updated_signal_obj = None
        if self.lightning_dialog:
            self.lightning_dialog.close()
            self.lightning_dialog = None
        if self.watchtower_dialog:
            self.watchtower_dialog.close()
            self.watchtower_dialog = None
        # Shut down the timer cleanly
        self.timer.stop()
        self.timer = None
        # clipboard persistence. see http://www.mail-archive.com/[email protected]/msg17328.html
        event = QtCore.QEvent(QtCore.QEvent.Clipboard)
        self.app.sendEvent(self.app.clipboard(), event)
        if self.tray:
            self.tray.hide()
            self.tray.deleteLater()
            self.tray = None

    def _maybe_quit_if_no_windows_open(self) -> None:
        """Check if there are any open windows and decide whether we should quit."""
        # keep daemon running after close
        if self.config.get('daemon'):
            return
        # check if a wizard is in progress
        with self._num_wizards_lock:
            if self._num_wizards_in_progress > 0 or len(self.windows) > 0:
                return
        self.app.quit()

    def new_window(self, path, uri=None):
        # Use a signal as can be called from daemon thread
        self.app.new_window_signal.emit(path, uri)

    def show_lightning_dialog(self):
        if not self.daemon.network.has_channel_db():
            return
        if not self.lightning_dialog:
            self.lightning_dialog = LightningDialog(self)
        self.lightning_dialog.bring_to_top()

    def show_watchtower_dialog(self):
        if not self.watchtower_dialog:
            self.watchtower_dialog = WatchtowerDialog(self)
        self.watchtower_dialog.bring_to_top()

    def show_network_dialog(self):
        if self.network_dialog:
            self.network_dialog.on_update()
            self.network_dialog.show()
            self.network_dialog.raise_()
            return
        self.network_dialog = NetworkDialog(
            network=self.daemon.network,
            config=self.config,
            network_updated_signal_obj=self.network_updated_signal_obj)
        self.network_dialog.show()

    def _create_window_for_wallet(self, wallet):
        w = ElectrumWindow(self, wallet)
        self.windows.append(w)
        self.build_tray_menu()
        w.warn_if_testnet()
        w.warn_if_watching_only()
        return w

    def count_wizards_in_progress(func):
        def wrapper(self: 'ElectrumGui', *args, **kwargs):
            with self._num_wizards_lock:
                self._num_wizards_in_progress += 1
            try:
                return func(self, *args, **kwargs)
            finally:
                with self._num_wizards_lock:
                    self._num_wizards_in_progress -= 1
                self._maybe_quit_if_no_windows_open()

        return wrapper

    @count_wizards_in_progress
    def start_new_window(
        self,
        path,
        uri: Optional[str],
        *,
        app_is_starting: bool = False,
        force_wizard: bool = False,
    ) -> Optional[ElectrumWindow]:
        '''Raises the window for the wallet if it is open.  Otherwise
        opens the wallet and creates a new window for it'''
        wallet = None
        # Try to open with daemon first. If this succeeds, there won't be a wizard at all
        # (the wallet main window will appear directly).
        if not force_wizard:
            try:
                wallet = self.daemon.load_wallet(path, None)
            except Exception as e:
                self.logger.exception('')
                custom_message_box(icon=QMessageBox.Warning,
                                   parent=None,
                                   title=_('Error'),
                                   text=_('Cannot load wallet') + ' (1):\n' +
                                   repr(e))
                # if app is starting, still let wizard appear
                if not app_is_starting:
                    return
        # Open a wizard window. This lets the user e.g. enter a password, or select
        # a different wallet.
        try:
            if not wallet:
                wallet = self._start_wizard_to_select_or_create_wallet(path)
            if not wallet:
                return
            # create or raise window
            for window in self.windows:
                if window.wallet.storage.path == wallet.storage.path:
                    break
            else:
                window = self._create_window_for_wallet(wallet)
        except Exception as e:
            self.logger.exception('')
            custom_message_box(icon=QMessageBox.Warning,
                               parent=None,
                               title=_('Error'),
                               text=_('Cannot load wallet') + '(2) :\n' +
                               repr(e))
            if app_is_starting:
                # If we raise in this context, there are no more fallbacks, we will shut down.
                # Worst case scenario, we might have gotten here without user interaction,
                # in which case, if we raise now without user interaction, the same sequence of
                # events is likely to repeat when the user restarts the process.
                # So we play it safe: clear path, clear uri, force a wizard to appear.
                try:
                    wallet_dir = os.path.dirname(path)
                    filename = get_new_wallet_name(wallet_dir)
                except OSError:
                    path = self.config.get_fallback_wallet_path()
                else:
                    path = os.path.join(wallet_dir, filename)
                self.start_new_window(path, uri=None, force_wizard=True)
            return
        if uri:
            window.pay_to_URI(uri)
        window.bring_to_top()
        window.setWindowState(window.windowState() & ~QtCore.Qt.WindowMinimized
                              | QtCore.Qt.WindowActive)

        window.activateWindow()
        return window

    def _start_wizard_to_select_or_create_wallet(
            self, path) -> Optional[Abstract_Wallet]:
        wizard = InstallWizard(self.config,
                               self.app,
                               self.plugins,
                               gui_object=self)
        try:
            path, storage = wizard.select_storage(path, self.daemon.get_wallet)
            # storage is None if file does not exist
            if storage is None:
                wizard.path = path  # needed by trustedcoin plugin
                wizard.run('new')
                storage, db = wizard.create_storage(path)
            else:
                db = WalletDB(storage.read(), manual_upgrades=False)
                wizard.run_upgrades(storage, db)
        except (UserCancelled, GoBack):
            return
        except WalletAlreadyOpenInMemory as e:
            return e.wallet
        finally:
            wizard.terminate()
        # return if wallet creation is not complete
        if storage is None or db.get_action():
            return
        wallet = Wallet(db, storage, config=self.config)
        wallet.start_network(self.daemon.network)
        self.daemon.add_wallet(wallet)
        return wallet

    def close_window(self, window: ElectrumWindow):
        if window in self.windows:
            self.windows.remove(window)
        self.build_tray_menu()
        # save wallet path of last open window
        if not self.windows:
            self.config.save_last_wallet(window.wallet)
        run_hook('on_close_window', window)
        self.daemon.stop_wallet(window.wallet.storage.path)

    def init_network(self):
        # Show network dialog if config does not exist
        if self.daemon.network:
            if self.config.get('auto_connect') is None:
                wizard = InstallWizard(self.config,
                                       self.app,
                                       self.plugins,
                                       gui_object=self)
                wizard.init_network(self.daemon.network)
                wizard.terminate()

    def main(self):
        # setup Ctrl-C handling and tear-down code first, so that user can easily exit whenever
        self.app.setQuitOnLastWindowClosed(
            False)  # so _we_ can decide whether to quit
        self.app.lastWindowClosed.connect(self._maybe_quit_if_no_windows_open)
        self.app.aboutToQuit.connect(self._cleanup_before_exit)
        signal.signal(signal.SIGINT, lambda *args: self.app.quit())
        # hook for crash reporter
        Exception_Hook.maybe_setup(config=self.config)
        # first-start network-setup
        try:
            self.init_network()
        except UserCancelled:
            return
        except GoBack:
            return
        except Exception as e:
            self.logger.exception('')
            return
        # start wizard to select/create wallet
        self.timer.start()
        path = self.config.get_wallet_path(use_gui_last_wallet=True)
        try:
            if not self.start_new_window(
                    path, self.config.get('url'), app_is_starting=True):
                return
        except Exception as e:
            self.logger.error(
                "error loading wallet (or creating window for it)")
            send_exception_to_crash_reporter(e)
            # Let Qt event loop start properly so that crash reporter window can appear.
            # We will shutdown when the user closes that window, via lastWindowClosed signal.
        # main loop
        self.logger.info("starting Qt main loop")
        self.app.exec_()
        # on some platforms the exec_ call may not return, so use _cleanup_before_exit

    def stop(self):
        self.logger.info('closing GUI')
        self.app.quit_signal.emit()
Beispiel #10
0
class MainWindow(QDialog, Ui_MainWindow):
    def __init__(self):
        super(MainWindow, self).__init__()
        self.setupUi(self)
        self.flagMove = False
        self.uiConfig = Ui_Config_Logic(self)  # 没有继承,会显示两个窗口,但是是模态的
        self.setWindowFlags(Qt.FramelessWindowHint)
        self.setFixedSize(89, 59)

        # self.animation = QPropertyAnimation(self, b'size')
        # self.animation.setDuration(1200)
        # self.animation.setStartValue(QSize(0, 0))
        # self.animation.setKeyValueAt(0.2, QSize(30, 20))
        # self.animation.setKeyValueAt(0.6, QSize(60, 40))
        # self.animation.setKeyValueAt(1, QSize(60, 40))
        # self.animation.setEndValue(QSize(289, 159))
        # self.animation.start()

        # 按钮的信号与槽
        self.btnConfig.clicked.connect(self.uiConfig.exec)
        self.btnMini.clicked.connect(self.slot_MainWindow_btns)
        self.btnScShot.clicked.connect(self.slot_MainWindow_btns)
        self.btnPrtSc.clicked.connect(self.slot_MainWindow_btns)
        self.loadConfig()
        # print(self.btnMini.isChecked())

        # 设置系统托盘
        self.tray = QSystemTrayIcon()  # 创建系统托盘对象
        self.tray.setIcon(QIcon('image/btnOk.png'))  # 设置系统托盘图标
        # self.tray.doubleClicked.connect(self.show)  # 设置托盘点击事件处理函数
        self.menuTray = QMenu(QApplication.desktop())  # 创建菜单
        self.actRestore = QAction(u'还原 ', self, triggered=self.showNormal)  # 添加一级菜单动作选项(还原主窗口)
        self.QuitAction = QAction(u'退出 ', self, triggered=self.close)  # 添加一级菜单动作选项(退出程序)
        self.menuTray.addAction(self.actRestore)  # 为菜单添加动作
        self.menuTray.addAction(self.QuitAction)
        self.tray.setContextMenu(self.menuTray)  # 设置系统托盘菜单

        # 添加系统热键,suppress默认是False,改为True后,就把系统中其它程序的热键阻塞了。
        keyboard.add_hotkey('ctrl+alt+a', self.btnScShot.click, suppress=False)


    def closeEvent(self, QCloseEvent):
        """保险起见,为了完整的退出"""
        self.tray.deleteLater()
        qApp.quit()

    def loadConfig(self):
        try:
            self.configDict = load(open('config.json', 'r', encoding='utf-8'))
            for k, v in self.configDict.items():
                eval(f"{k}({v})")
        except Exception as e:
            print(e)

    def slot_MainWindow_btns(self):
        sender = self.sender()
        if sender == self.btnMini:
            if self.uiConfig.ccBoxMiniToTray.checkState():  # 是否最小化到托盘
                self.hide()
        elif sender == self.btnScShot:
            self.uiScShot = Ui_ScShot_Logic(self.uiConfig)
            self.uiScShot.show()
        elif sender == self.btnPrtSc:
            if self.uiConfig.ccBoxAppHide.checkState():
                self.hide()

            prtSc = QApplication.primaryScreen().grabWindow(0)  # 截取整个屏幕,QPixmap类型
            filePath = self.uiConfig.lineEdFilePath.text()
            if not os.path.exists(filePath):  # 如果此路径没有此文件夹,就创建一个
                os.mkdir(filePath)
            rq = time.strftime('%Y%m%d%H%M%S', time.localtime(time.time()))
            imgPath = f"{filePath}/{rq}.png"
            prtSc.save(imgPath, format='png', quality=100)
            PlaySound('prtSc.wav', flags=1)

            if self.uiConfig.ccBoxAppShow.checkState():
                self.showNormal()

    def mousePressEvent(self, event):
        if event.button() == Qt.RightButton:
            self.flagMove = True
            self.m_Position = event.globalPos() - self.pos()  # 获取鼠标相对窗口的位置
            event.accept()
            self.setCursor(QCursor(Qt.OpenHandCursor))  # 更改鼠标图标

    def mouseMoveEvent(self, QMouseEvent):
        if Qt.RightButton and self.flagMove:
            self.move(QMouseEvent.globalPos() - self.m_Position)  # 更改窗口位置
            QMouseEvent.accept()

    def mouseReleaseEvent(self, QMouseEvent):
        self.flagMove = False
        self.setCursor(QCursor(Qt.CustomCursor))  # 更改鼠标图标为系统默认   CrossCursor十字
Beispiel #11
0
class MainWindow(QMainWindow):
    EXIT_CODE_REBOOT = 520

    def __init__(self):
        super().__init__()
        Config.initialize()
        self.initUI()

    def initUI(self):
        self.setAttribute(Qt.WA_DeleteOnClose)
        self.spliter = QSplitter(Qt.Vertical)
        self.spliter.addWidget(TestUnitArea())
        self.spliter.addWidget(TestResultArea())
        self.spliter.setHandleWidth(1)
        self.setCentralWidget(self.spliter)

        tool_menu = QMenu('工具', self.menuBar())
        tool_menu.addAction('数据监听', self.onDebugWindow)
        tool_menu.addAction('单步测试', self.onSingleStep)
        tool_menu.addAction('记录查询', self.onViewData)
        tool_menu.addAction('条码打印', self.onPrintBarCode)
        tool_menu.addAction('异常信息', self.onExceptionWindow)

        setting_menu = QMenu('选项', self.menuBar())
        setting_menu.addAction('参数设置', self.onSetting)
        # setting_menu.addAction('软件重启', self.onRestart)

        help_menu = QMenu('帮助', self.menuBar())
        help_menu.addAction('关于', self.onAbout)

        self.menuBar().addMenu(setting_menu)
        self.menuBar().addMenu(tool_menu)
        self.menuBar().addMenu(help_menu)

        QApplication.setWindowIcon(QIcon(Config.LOGO_IMG))
        QApplication.instance().aboutToQuit.connect(self.onApplicationQuit)
        QApplication.setOrganizationName(Config.ORGANIZATION)
        QApplication.setApplicationName(Config.APP_NAME)
        QApplication.setApplicationVersion(Config.APP_VERSION)
        self.restoreQSettings()
        self.createSystemTray()

    def onDebugWindow(self):
        if not ui.DebugDialog.prev_actived:
            self.debugWin = ui.DebugDialog()
            self.debugWin.show()
        else:
            QApplication.setActiveWindow(ui.DebugDialog.prev_window)
            ui.DebugDialog.prev_window.showNormal()

    def onSingleStep(self):
        if not ui.SingleStepFrame.prev_actived:
            self.singleWin = ui.SingleStepFrame()
            self.singleWin.show()
        else:
            QApplication.setActiveWindow(ui.SingleStepFrame.prev_window)
            ui.SingleStepFrame.prev_window.showNormal()

    def onViewData(self):
        if not ui.SearchWindow.prev_actived:
            self.searchWin = ui.SearchWindow()
            self.searchWin.show()
        else:
            QApplication.setActiveWindow(ui.SearchWindow.prev_window)
            ui.SearchWindow.prev_window.showNormal()

    def onPrintBarCode(self):
        if not CodeDialog.prev_actived:
            self.codeWin = CodeDialog()
            self.codeWin.show()
        else:
            QApplication.setActiveWindow(CodeDialog.prev_window)
            CodeDialog.prev_window.showNormal()

    def onExceptionWindow(self):
        if not ui.ExceptionWindow.prev_actived:
            self.excptionWin = ui.ExceptionWindow()
            self.excptionWin.show()
        else:
            QApplication.setActiveWindow(ui.ExceptionWindow.prev_window)
            ui.ExceptionWindow.prev_window.showNormal()

    def restoreQSettings(self):
        main_win_geo = Config.QSETTING.value('MainWindow/geometry')
        main_win_centerwgt_state = Config.QSETTING.value(
            'MainWindow/CenterWidget/state')

        if main_win_geo: self.restoreGeometry(main_win_geo)
        if main_win_centerwgt_state:
            self.spliter.restoreState(main_win_centerwgt_state)

    def onSetting(self):
        dlg = ui.SettingDialog(self)
        dlg.move(self.x() + 50, self.y() + 50)
        dlg.exec()

    def onRestart(self):
        QApplication.exit(self.EXIT_CODE_REBOOT)

    def onAbout(self):
        dlg = ui.AboutDialog(Config.ABOUT_HTML)
        dlg.resize(400, 300)
        dlg.exec()

    def createSystemTray(self):
        self.systray = QSystemTrayIcon(self)
        self.systray.setIcon(QIcon(Config.LOGO_IMG))
        self.systray.show()

        trayMenu = QMenu()
        trayMenu.addAction('最大化', self.showMaximized)
        trayMenu.addAction('最小化', self.showMinimized)
        trayMenu.addAction('显示窗口', self.showNormal)
        stayOnTop = QAction('总在最前',
                            trayMenu,
                            checkable=True,
                            triggered=self.stayOnTop)
        trayMenu.addAction(stayOnTop)
        trayMenu.addSeparator()
        trayMenu.addAction('退出', QApplication.quit)

        username = platform.node()
        ip = socket.gethostbyname(socket.gethostname())

        self.systray.setToolTip('用户:{}\nIP:{}'.format(username, ip))
        self.systray.activated.connect(self.onSystemTrayActivated)
        self.systray.setContextMenu(trayMenu)

    def onSystemTrayActivated(self, reason):
        if reason in [QSystemTrayIcon.DoubleClick, QSystemTrayIcon.Trigger]:
            self.showNormal()

    def stayOnTop(self, checked):
        self.setWindowFlag(Qt.WindowStaysOnTopHint, self.sender().isChecked())
        self.show()

    def onApplicationQuit(self):
        Config.QSETTING.setValue('MainWindow/geometry', self.saveGeometry())
        Config.QSETTING.setValue('MainWindow/CenterWidget/state',
                                 self.spliter.saveState())
        Config.finalize()
        self.systray.deleteLater()

    def closeEvent(self, event):
        Config.QSETTING.setValue('MainWindow/geometry', self.saveGeometry())
        Config.QSETTING.setValue('MainWindow/CenterWidget/state',
                                 self.spliter.saveState())

        if self.systray.isVisible():
            self.hide()
            event.ignore()
Beispiel #12
0
class XNova_MainWindow(QWidget):

    STATE_NOT_AUTHED = 0
    STATE_AUTHED = 1

    def __init__(self, parent=None):
        super(XNova_MainWindow, self).__init__(parent, Qt.Window)
        # state vars
        self.config_store_dir = './cache'
        self.cfg = configparser.ConfigParser()
        self.cfg.read('config/net.ini', encoding='utf-8')
        self.state = self.STATE_NOT_AUTHED
        self.login_email = ''
        self.cookies_dict = {}
        self._hidden_to_tray = False
        #
        # init UI
        self.setWindowIcon(QIcon(':/i/xnova_logo_64.png'))
        self.setWindowTitle('XNova Commander')
        # main layouts
        self._layout = QVBoxLayout()
        self._layout.setContentsMargins(0, 2, 0, 0)
        self._layout.setSpacing(3)
        self.setLayout(self._layout)
        self._horizontal_layout = QHBoxLayout()
        self._horizontal_layout.setContentsMargins(0, 0, 0, 0)
        self._horizontal_layout.setSpacing(6)
        # flights frame
        self._fr_flights = QFrame(self)
        self._fr_flights.setMinimumHeight(22)
        self._fr_flights.setFrameShape(QFrame.NoFrame)
        self._fr_flights.setFrameShadow(QFrame.Plain)
        # planets bar scrollarea
        self._sa_planets = QScrollArea(self)
        self._sa_planets.setMinimumWidth(125)
        self._sa_planets.setMaximumWidth(125)
        self._sa_planets.setFrameShape(QFrame.NoFrame)
        self._sa_planets.setFrameShadow(QFrame.Plain)
        self._sa_planets.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
        self._sa_planets.setWidgetResizable(True)
        self._panel_planets = QWidget(self._sa_planets)
        self._layout_pp = QVBoxLayout()
        self._panel_planets.setLayout(self._layout_pp)
        self._lbl_planets = QLabel(self.tr('Planets:'), self._panel_planets)
        self._lbl_planets.setMaximumHeight(32)
        self._layout_pp.addWidget(self._lbl_planets)
        self._layout_pp.addStretch()
        self._sa_planets.setWidget(self._panel_planets)
        #
        # tab widget
        self._tabwidget = XTabWidget(self)
        self._tabwidget.enableButtonAdd(False)
        self._tabwidget.tabCloseRequested.connect(self.on_tab_close_requested)
        self._tabwidget.addClicked.connect(self.on_tab_add_clicked)
        #
        # create status bar
        self._statusbar = XNCStatusBar(self)
        self.set_status_message(self.tr('Not connected: Log in!'))
        #
        # tab widget pages
        self.login_widget = None
        self.flights_widget = None
        self.overview_widget = None
        self.imperium_widget = None
        #
        # settings widget
        self.settings_widget = SettingsWidget(self)
        self.settings_widget.settings_changed.connect(self.on_settings_changed)
        self.settings_widget.hide()
        #
        # finalize layouts
        self._horizontal_layout.addWidget(self._sa_planets)
        self._horizontal_layout.addWidget(self._tabwidget)
        self._layout.addWidget(self._fr_flights)
        self._layout.addLayout(self._horizontal_layout)
        self._layout.addWidget(self._statusbar)
        #
        # system tray icon
        self.tray_icon = None
        show_tray_icon = False
        if 'tray' in self.cfg:
            if (self.cfg['tray']['icon_usage'] == 'show') or \
                    (self.cfg['tray']['icon_usage'] == 'show_min'):
                self.create_tray_icon()
        #
        # try to restore last window size
        ssz = self.load_cfg_val('main_size')
        if ssz is not None:
            self.resize(ssz[0], ssz[1])
        #
        # world initialization
        self.world = XNovaWorld_instance()
        self.world_timer = QTimer(self)
        self.world_timer.timeout.connect(self.on_world_timer)

    # overrides QWidget.closeEvent
    # cleanup just before the window close
    def closeEvent(self, close_event: QCloseEvent):
        logger.debug('closing')
        if self.tray_icon is not None:
            self.tray_icon.hide()
            self.tray_icon = None
        if self.world_timer.isActive():
            self.world_timer.stop()
        self.world.script_command = 'stop'  # also stop possible running scripts
        if self.world.isRunning():
            self.world.quit()
            logger.debug('waiting for world thread to stop (5 sec)...')
            wait_res = self.world.wait(5000)
            if not wait_res:
                logger.warn('wait failed, last chance, terminating!')
                self.world.terminate()
        # store window size
        ssz = (self.width(), self.height())
        self.store_cfg_val('main_size', ssz)
        # accept the event
        close_event.accept()

    def showEvent(self, evt: QShowEvent):
        super(XNova_MainWindow, self).showEvent(evt)
        self._hidden_to_tray = False

    def changeEvent(self, evt: QEvent):
        super(XNova_MainWindow, self).changeEvent(evt)
        if evt.type() == QEvent.WindowStateChange:
            if not isinstance(evt, QWindowStateChangeEvent):
                return
            # make sure we only do this for minimize events
            if (evt.oldState() != Qt.WindowMinimized) and self.isMinimized():
                # we were minimized! explicitly hide settings widget
                #     if it is open, otherwise it will be lost forever :(
                if self.settings_widget is not None:
                    if self.settings_widget.isVisible():
                        self.settings_widget.hide()
                # should we minimize to tray?
                if self.cfg['tray']['icon_usage'] == 'show_min':
                    if not self._hidden_to_tray:
                        self._hidden_to_tray = True
                        self.hide()

    def create_tray_icon(self):
        if QSystemTrayIcon.isSystemTrayAvailable():
            logger.debug('System tray icon is available, showing')
            self.tray_icon = QSystemTrayIcon(QIcon(':/i/xnova_logo_32.png'),
                                             self)
            self.tray_icon.setToolTip(self.tr('XNova Commander'))
            self.tray_icon.activated.connect(self.on_tray_icon_activated)
            self.tray_icon.show()
        else:
            self.tray_icon = None

    def hide_tray_icon(self):
        if self.tray_icon is not None:
            self.tray_icon.hide()
            self.tray_icon.deleteLater()
            self.tray_icon = None

    def set_tray_tooltip(self, tip: str):
        if self.tray_icon is not None:
            self.tray_icon.setToolTip(tip)

    def set_status_message(self, msg: str):
        self._statusbar.set_status(msg)

    def store_cfg_val(self, category: str, value):
        pickle_filename = '{0}/{1}.dat'.format(self.config_store_dir, category)
        try:
            cache_dir = pathlib.Path(self.config_store_dir)
            if not cache_dir.exists():
                cache_dir.mkdir()
            with open(pickle_filename, 'wb') as f:
                pickle.dump(value, f)
        except pickle.PickleError as pe:
            pass
        except IOError as ioe:
            pass

    def load_cfg_val(self, category: str, default_value=None):
        value = None
        pickle_filename = '{0}/{1}.dat'.format(self.config_store_dir, category)
        try:
            with open(pickle_filename, 'rb') as f:
                value = pickle.load(f)
                if value is None:
                    value = default_value
        except pickle.PickleError as pe:
            pass
        except IOError as ioe:
            pass
        return value

    @pyqtSlot()
    def on_settings_changed(self):
        self.cfg.read('config/net.ini', encoding='utf-8')
        # maybe show/hide tray icon now?
        show_tray_icon = False
        if 'tray' in self.cfg:
            icon_usage = self.cfg['tray']['icon_usage']
            if (icon_usage == 'show') or (icon_usage == 'show_min'):
                show_tray_icon = True
        # show if needs show and hidden, or hide if shown and needs to hide
        if show_tray_icon and (self.tray_icon is None):
            logger.debug('settings changed, showing tray icon')
            self.create_tray_icon()
        elif (not show_tray_icon) and (self.tray_icon is not None):
            logger.debug('settings changed, hiding tray icon')
            self.hide_tray_icon()
        # also notify world about changed config!
        self.world.reload_config()

    def add_tab(self,
                widget: QWidget,
                title: str,
                closeable: bool = True) -> int:
        tab_index = self._tabwidget.addTab(widget, title, closeable)
        return tab_index

    def remove_tab(self, index: int):
        self._tabwidget.removeTab(index)

    # called by main application object just after main window creation
    # to show login widget and begin login process
    def begin_login(self):
        # create flights widget
        self.flights_widget = FlightsWidget(self._fr_flights)
        self.flights_widget.load_ui()
        install_layout_for_widget(self._fr_flights,
                                  Qt.Vertical,
                                  margins=(1, 1, 1, 1),
                                  spacing=1)
        self._fr_flights.layout().addWidget(self.flights_widget)
        self.flights_widget.set_online_state(False)
        self.flights_widget.requestShowSettings.connect(self.on_show_settings)
        # create and show login widget as first tab
        self.login_widget = LoginWidget(self._tabwidget)
        self.login_widget.load_ui()
        self.login_widget.loginError.connect(self.on_login_error)
        self.login_widget.loginOk.connect(self.on_login_ok)
        self.login_widget.show()
        self.add_tab(self.login_widget, self.tr('Login'), closeable=False)
        # self.test_setup_planets_panel()
        # self.test_planet_tab()

    def setup_planets_panel(self, planets: list):
        layout = self._panel_planets.layout()
        layout.setSpacing(0)
        remove_trailing_spacer_from_layout(layout)
        # remove all previous planet widgets from planets panel
        if layout.count() > 0:
            for i in range(layout.count() - 1, -1, -1):
                li = layout.itemAt(i)
                if li is not None:
                    wi = li.widget()
                    if wi is not None:
                        if isinstance(wi, PlanetSidebarWidget):
                            layout.removeWidget(wi)
                            wi.close()
                            wi.deleteLater()  # fix possible mem leak
                            del wi
        for pl in planets:
            pw = PlanetSidebarWidget(self._panel_planets)
            pw.setPlanet(pl)
            layout.addWidget(pw)
            pw.show()
            # connections from each planet bar widget
            pw.requestOpenGalaxy.connect(self.on_request_open_galaxy_tab)
            pw.requestOpenPlanet.connect(self.on_request_open_planet_tab)
        append_trailing_spacer_to_layout(layout)

    def update_planets_panel(self):
        """
        Calls QWidget.update() on every PlanetBarWidget
        embedded in ui.panel_planets, causing repaint
        """
        layout = self._panel_planets.layout()
        if layout.count() > 0:
            for i in range(layout.count()):
                li = layout.itemAt(i)
                if li is not None:
                    wi = li.widget()
                    if wi is not None:
                        if isinstance(wi, PlanetSidebarWidget):
                            wi.update()

    def add_tab_for_planet(self, planet: XNPlanet):
        # construct planet widget and setup signals/slots
        plw = PlanetWidget(self._tabwidget)
        plw.requestOpenGalaxy.connect(self.on_request_open_galaxy_tab)
        plw.setPlanet(planet)
        # construct tab title
        tab_title = '{0} {1}'.format(planet.name, planet.coords.coords_str())
        # add tab and make it current
        tab_index = self.add_tab(plw, tab_title, closeable=True)
        self._tabwidget.setCurrentIndex(tab_index)
        self._tabwidget.tabBar().setTabIcon(tab_index,
                                            QIcon(':/i/planet_32.png'))
        return tab_index

    def add_tab_for_galaxy(self, coords: XNCoords = None):
        gw = GalaxyWidget(self._tabwidget)
        tab_title = '{0}'.format(self.tr('Galaxy'))
        if coords is not None:
            tab_title = '{0} {1}'.format(self.tr('Galaxy'),
                                         coords.coords_str())
            gw.setCoords(coords.galaxy, coords.system)
        idx = self.add_tab(gw, tab_title, closeable=True)
        self._tabwidget.setCurrentIndex(idx)
        self._tabwidget.tabBar().setTabIcon(idx, QIcon(':/i/galaxy_32.png'))

    @pyqtSlot(int)
    def on_tab_close_requested(self, idx: int):
        # logger.debug('tab close requested: {0}'.format(idx))
        if idx <= 1:  # cannot close overview or imperium tabs
            return
        self.remove_tab(idx)

    @pyqtSlot()
    def on_tab_add_clicked(self):
        pos = QCursor.pos()
        planets = self.world.get_planets()
        # logger.debug('tab bar add clicked, cursor pos = ({0}, {1})'.format(pos.x(), pos.y()))
        menu = QMenu(self)
        # galaxy view
        galaxy_action = QAction(menu)
        galaxy_action.setText(self.tr('Add galaxy view'))
        galaxy_action.setData(QVariant('galaxy'))
        menu.addAction(galaxy_action)
        # planets
        menu.addSection(self.tr('-- Planet tabs: --'))
        for planet in planets:
            action = QAction(menu)
            action.setText('{0} {1}'.format(planet.name,
                                            planet.coords.coords_str()))
            action.setData(QVariant(planet.planet_id))
            menu.addAction(action)
        action_ret = menu.exec(pos)
        if action_ret is not None:
            # logger.debug('selected action data = {0}'.format(str(action_ret.data())))
            if action_ret == galaxy_action:
                logger.debug('action_ret == galaxy_action')
                self.add_tab_for_galaxy()
                return
            # else consider this is planet widget
            planet_id = int(action_ret.data())
            self.on_request_open_planet_tab(planet_id)

    @pyqtSlot(str)
    def on_login_error(self, errstr):
        logger.error('Login error: {0}'.format(errstr))
        self.state = self.STATE_NOT_AUTHED
        self.set_status_message(self.tr('Login error: {0}').format(errstr))
        QMessageBox.critical(self, self.tr('Login error:'), errstr)

    @pyqtSlot(str, dict)
    def on_login_ok(self, login_email, cookies_dict):
        # logger.debug('Login OK, login: {0}, cookies: {1}'.format(login_email, str(cookies_dict)))
        # save login data: email, cookies
        self.state = self.STATE_AUTHED
        self.set_status_message(self.tr('Login OK, loading world'))
        self.login_email = login_email
        self.cookies_dict = cookies_dict
        #
        # destroy login widget and remove its tab
        self.remove_tab(0)
        self.login_widget.close()
        self.login_widget.deleteLater()
        self.login_widget = None
        #
        # create overview widget and add it as first tab
        self.overview_widget = OverviewWidget(self._tabwidget)
        self.overview_widget.load_ui()
        self.add_tab(self.overview_widget,
                     self.tr('Overview'),
                     closeable=False)
        self.overview_widget.show()
        self.overview_widget.setEnabled(False)
        #
        # create 2nd tab - Imperium
        self.imperium_widget = ImperiumWidget(self._tabwidget)
        self.add_tab(self.imperium_widget,
                     self.tr('Imperium'),
                     closeable=False)
        self.imperium_widget.setEnabled(False)
        #
        # initialize XNova world updater
        self.world.initialize(cookies_dict)
        self.world.set_login_email(self.login_email)
        # connect signals from world
        self.world.world_load_progress.connect(self.on_world_load_progress)
        self.world.world_load_complete.connect(self.on_world_load_complete)
        self.world.net_request_started.connect(self.on_net_request_started)
        self.world.net_request_finished.connect(self.on_net_request_finished)
        self.world.flight_arrived.connect(self.on_flight_arrived)
        self.world.build_complete.connect(self.on_building_complete)
        self.world.loaded_overview.connect(self.on_loaded_overview)
        self.world.loaded_imperium.connect(self.on_loaded_imperium)
        self.world.loaded_planet.connect(self.on_loaded_planet)
        self.world.start()

    @pyqtSlot(str, int)
    def on_world_load_progress(self, comment: str, progress: int):
        self._statusbar.set_world_load_progress(comment, progress)

    @pyqtSlot()
    def on_world_load_complete(self):
        logger.debug('main: on_world_load_complete()')
        # enable adding new tabs
        self._tabwidget.enableButtonAdd(True)
        # update statusbar
        self._statusbar.set_world_load_progress(
            '', -1)  # turn off progress display
        self.set_status_message(self.tr('World loaded.'))
        # update account info
        if self.overview_widget is not None:
            self.overview_widget.setEnabled(True)
            self.overview_widget.update_account_info()
            self.overview_widget.update_builds()
        # update flying fleets
        self.flights_widget.set_online_state(True)
        self.flights_widget.update_flights()
        # update planets
        planets = self.world.get_planets()
        self.setup_planets_panel(planets)
        if self.imperium_widget is not None:
            self.imperium_widget.setEnabled(True)
            self.imperium_widget.update_planets()
        # update statusbar
        self._statusbar.update_online_players_count()
        # update tray tooltip, add account name
        self.set_tray_tooltip(
            self.tr('XNova Commander') + ' - ' +
            self.world.get_account_info().login)
        # set timer to do every-second world recalculation
        self.world_timer.setInterval(1000)
        self.world_timer.setSingleShot(False)
        self.world_timer.start()

    @pyqtSlot()
    def on_loaded_overview(self):
        logger.debug('on_loaded_overview')
        # A lot of things are updated when overview is loaded
        #  * Account information and stats
        if self.overview_widget is not None:
            self.overview_widget.update_account_info()
        #  * flights will be updated every second anyway in on_world_timer(), so no need to call
        #    self.flights_widget.update_flights()
        #  * messages count also, is updated with flights
        #  * current planet may have changed
        self.update_planets_panel()
        #  * server time is updated also
        self._statusbar.update_online_players_count()

    @pyqtSlot()
    def on_loaded_imperium(self):
        logger.debug('on_loaded_imperium')
        # need to update imperium widget
        if self.imperium_widget is not None:
            self.imperium_widget.update_planets()
        # The important note here is that imperium update is the only place where
        # the planets list is read, so number of planets, their names, etc may change here
        # Also, imperium update OVERWRITES full planets array, so, all prev
        # references to planets in all GUI elements must be invalidated, because
        # they will point to unused, outdated planets
        planets = self.world.get_planets()
        # re-create planets sidebar
        self.setup_planets_panel(planets)
        # update all builds in overview widget
        if self.overview_widget:
            self.overview_widget.update_builds()
        # update all planet tabs with new planet references
        cnt = self._tabwidget.count()
        if cnt > 2:
            for index in range(2, cnt):
                tab_page = self._tabwidget.tabWidget(index)
                if tab_page is not None:
                    try:
                        tab_type = tab_page.get_tab_type()
                        if tab_type == 'planet':
                            tab_planet = tab_page.planet()
                            new_planet = self.world.get_planet(
                                tab_planet.planet_id)
                            tab_page.setPlanet(new_planet)
                    except AttributeError:  # not all pages may have method get_tab_type()
                        pass

    @pyqtSlot(int)
    def on_loaded_planet(self, planet_id: int):
        logger.debug('Got signal on_loaded_planet({0}), updating overview '
                     'widget and planets panel'.format(planet_id))
        if self.overview_widget:
            self.overview_widget.update_builds()
        self.update_planets_panel()
        # update also planet tab, if any
        planet = self.world.get_planet(planet_id)
        if planet is not None:
            tab_idx = self.find_tab_for_planet(planet_id)
            if tab_idx != -1:
                tab_widget = self._tabwidget.tabWidget(tab_idx)
                if isinstance(tab_widget, PlanetWidget):
                    logger.debug('Updating planet tab #{}'.format(tab_idx))
                    tab_widget.setPlanet(planet)

    @pyqtSlot()
    def on_world_timer(self):
        if self.world:
            self.world.world_tick()
        self.update_planets_panel()
        if self.flights_widget:
            self.flights_widget.update_flights()
        if self.overview_widget:
            self.overview_widget.update_builds()
        if self.imperium_widget:
            self.imperium_widget.update_planet_resources()

    @pyqtSlot()
    def on_net_request_started(self):
        self._statusbar.set_loading_status(True)

    @pyqtSlot()
    def on_net_request_finished(self):
        self._statusbar.set_loading_status(False)

    @pyqtSlot(int)
    def on_tray_icon_activated(self, reason):
        # QSystemTrayIcon::Unknown	0	Unknown reason
        # QSystemTrayIcon::Context	1	The context menu for the system tray entry was requested
        # QSystemTrayIcon::DoubleClick	2	The system tray entry was double clicked
        # QSystemTrayIcon::Trigger	3	The system tray entry was clicked
        # QSystemTrayIcon::MiddleClick	4	The system tray entry was clicked with the middle mouse button
        if reason == QSystemTrayIcon.Trigger:
            # left-click
            self.setWindowState((self.windowState() & ~Qt.WindowMinimized)
                                | Qt.WindowActive)
            self.show()
            return

    def show_tray_message(self,
                          title,
                          message,
                          icon_type=None,
                          timeout_ms=None):
        """
        Shows message from system tray icon, if system supports it.
        If no support, this is just a no-op
        :param title: message title
        :param message: message text
        :param icon_type: one of:
        QSystemTrayIcon.NoIcon      0   No icon is shown.
        QSystemTrayIcon.Information 1   An information icon is shown.
        QSystemTrayIcon.Warning     2   A standard warning icon is shown.
        QSystemTrayIcon.Critical    3   A critical warning icon is shown
        """
        if self.tray_icon is None:
            return
        if self.tray_icon.supportsMessages():
            if icon_type is None:
                icon_type = QSystemTrayIcon.Information
            if timeout_ms is None:
                timeout_ms = 10000
            self.tray_icon.showMessage(title, message, icon_type, timeout_ms)
        else:
            logger.info('This system does not support tray icon messages.')

    @pyqtSlot()
    def on_show_settings(self):
        if self.settings_widget is not None:
            self.settings_widget.show()
            self.settings_widget.showNormal()

    @pyqtSlot(XNFlight)
    def on_flight_arrived(self, fl: XNFlight):
        logger.debug('main: flight arrival: {0}'.format(fl))
        mis_str = flight_mission_for_humans(fl.mission)
        if fl.direction == 'return':
            mis_str += ' ' + self.tr('return')
        short_fleet_info = self.tr('{0} {1} => {2}, {3} ship(s)').format(
            mis_str, fl.src, fl.dst, len(fl.ships))
        self.show_tray_message(self.tr('XNova: Fleet arrived'),
                               short_fleet_info)

    @pyqtSlot(XNPlanet, XNPlanetBuildingItem)
    def on_building_complete(self, planet: XNPlanet,
                             bitem: XNPlanetBuildingItem):
        logger.debug('main: build complete: on planet {0}: {1}'.format(
            planet.name, str(bitem)))
        # update also planet tab, if any
        if isinstance(planet, XNPlanet):
            tab_idx = self.find_tab_for_planet(planet.planet_id)
            if tab_idx != -1:
                tab_widget = self._tabwidget.tabWidget(tab_idx)
                if isinstance(tab_widget, PlanetWidget):
                    logger.debug('Updating planet tab #{}'.format(tab_idx))
                    tab_widget.setPlanet(planet)
        # construct message to show in tray
        if bitem.is_shipyard_item:
            binfo_str = '{0} x {1}'.format(bitem.quantity, bitem.name)
        else:
            binfo_str = self.tr('{0} lv.{1}').format(bitem.name, bitem.level)
        msg = self.tr('{0} has built {1}').format(planet.name, binfo_str)
        self.show_tray_message(self.tr('XNova: Building complete'), msg)

    @pyqtSlot(XNCoords)
    def on_request_open_galaxy_tab(self, coords: XNCoords):
        tab_index = self.find_tab_for_galaxy(coords.galaxy, coords.system)
        if tab_index == -1:  # create new tab for these coords
            self.add_tab_for_galaxy(coords)
            return
        # else switch to that tab
        self._tabwidget.setCurrentIndex(tab_index)

    @pyqtSlot(int)
    def on_request_open_planet_tab(self, planet_id: int):
        tab_index = self.find_tab_for_planet(planet_id)
        if tab_index == -1:  # create new tab for planet
            planet = self.world.get_planet(planet_id)
            if planet is not None:
                self.add_tab_for_planet(planet)
                return
        # else switch to that tab
        self._tabwidget.setCurrentIndex(tab_index)

    def find_tab_for_planet(self, planet_id: int) -> int:
        """
        Finds tab index where specified planet is already opened
        :param planet_id: planet id to search for
        :return: tab index, or -1 if not found
        """
        cnt = self._tabwidget.count()
        if cnt < 3:
            return -1  # only overview and imperium tabs are present
        for index in range(2, cnt):
            tab_page = self._tabwidget.tabWidget(index)
            if tab_page is not None:
                try:
                    tab_type = tab_page.get_tab_type()
                    if tab_type == 'planet':
                        tab_planet = tab_page.planet()
                        if tab_planet.planet_id == planet_id:
                            # we have found tab index where this planet is already opened
                            return index
                except AttributeError:  # not all pages may have method get_tab_type()
                    pass
        return -1

    def find_tab_for_galaxy(self, galaxy: int, system: int) -> int:
        """
        Finds tab index where specified galaxy view is already opened
        :param galaxy: galaxy target coordinate
        :param system: system target coordinate
        :return: tab index, or -1 if not found
        """
        cnt = self._tabwidget.count()
        if cnt < 3:
            return -1  # only overview and imperium tabs are present
        for index in range(2, cnt):
            tab_page = self._tabwidget.tabWidget(index)
            if tab_page is not None:
                try:
                    tab_type = tab_page.get_tab_type()
                    if tab_type == 'galaxy':
                        coords = tab_page.coords()
                        if (coords[0] == galaxy) and (coords[1] == system):
                            # we have found galaxy tab index where this place is already opened
                            return index
                except AttributeError:  # not all pages may have method get_tab_type()
                    pass
        return -1

    def test_setup_planets_panel(self):
        """
        Testing only - add 'fictive' planets to test planets panel without loading data
        :return: None
        """
        pl1 = XNPlanet('Arnon', XNCoords(1, 7, 6))
        pl1.pic_url = 'skins/default/planeten/small/s_normaltempplanet08.jpg'
        pl1.fields_busy = 90
        pl1.fields_total = 167
        pl1.is_current = True
        pl2 = XNPlanet('Safizon', XNCoords(1, 232, 7))
        pl2.pic_url = 'skins/default/planeten/small/s_dschjungelplanet05.jpg'
        pl2.fields_busy = 84
        pl2.fields_total = 207
        pl2.is_current = False
        test_planets = [pl1, pl2]
        self.setup_planets_panel(test_planets)

    def test_planet_tab(self):
        """
        Testing only - add 'fictive' planet tab to test UI without loading world
        :return:
        """
        # construct planet
        pl1 = XNPlanet('Arnon', coords=XNCoords(1, 7, 6), planet_id=12345)
        pl1.pic_url = 'skins/default/planeten/small/s_normaltempplanet08.jpg'
        pl1.fields_busy = 90
        pl1.fields_total = 167
        pl1.is_current = True
        pl1.res_current.met = 10000000
        pl1.res_current.cry = 50000
        pl1.res_current.deit = 250000000  # 250 mil
        pl1.res_per_hour.met = 60000
        pl1.res_per_hour.cry = 30000
        pl1.res_per_hour.deit = 15000
        pl1.res_max_silos.met = 6000000
        pl1.res_max_silos.cry = 3000000
        pl1.res_max_silos.deit = 1000000
        pl1.energy.energy_left = 10
        pl1.energy.energy_total = 1962
        pl1.energy.charge_percent = 92
        # planet building item
        bitem = XNPlanetBuildingItem()
        bitem.gid = 1
        bitem.name = 'Рудник металла'
        bitem.level = 29
        bitem.remove_link = ''
        bitem.build_link = '?set=buildings&cmd=insert&building={0}'.format(
            bitem.gid)
        bitem.seconds_total = 23746
        bitem.cost_met = 7670042
        bitem.cost_cry = 1917510
        bitem.is_building_item = True
        # second bitem
        bitem2 = XNPlanetBuildingItem()
        bitem2.gid = 2
        bitem2.name = 'Рудник кристалла'
        bitem2.level = 26
        bitem2.remove_link = ''
        bitem2.build_link = '?set=buildings&cmd=insert&building={0}'.format(
            bitem2.gid)
        bitem2.seconds_total = 13746
        bitem2.cost_met = 9735556
        bitem2.cost_cry = 4667778
        bitem2.is_building_item = True
        bitem2.is_downgrade = True
        bitem2.seconds_left = bitem2.seconds_total // 2
        bitem2.calc_end_time()
        # add bitems
        pl1.buildings_items = [bitem, bitem2]
        # add
        self.add_tab_for_planet(pl1)
Beispiel #13
0
class TriblerWindow(QMainWindow):
    resize_event = pyqtSignal()
    escape_pressed = pyqtSignal()
    tribler_crashed = pyqtSignal(str)
    received_search_completions = pyqtSignal(object)

    def on_exception(self, *exc_info):
        if self.exception_handler_called:
            # We only show one feedback dialog, even when there are two consecutive exceptions.
            return

        self.exception_handler_called = True

        exception_text = "".join(traceback.format_exception(*exc_info))
        logging.error(exception_text)
        self.tribler_crashed.emit(exception_text)

        self.delete_tray_icon()

        # Stop the download loop
        self.downloads_page.stop_loading_downloads()

        # Add info about whether we are stopping Tribler or not
        os.environ['TRIBLER_SHUTTING_DOWN'] = str(self.core_manager.shutting_down)

        if not self.core_manager.shutting_down:
            self.core_manager.stop(stop_app_on_shutdown=False)

        self.setHidden(True)

        if self.debug_window:
            self.debug_window.setHidden(True)

        dialog = FeedbackDialog(self, exception_text, self.core_manager.events_manager.tribler_version,
                                self.start_time)
        dialog.show()

    def __init__(self, core_args=None, core_env=None, api_port=None):
        QMainWindow.__init__(self)

        QCoreApplication.setOrganizationDomain("nl")
        QCoreApplication.setOrganizationName("TUDelft")
        QCoreApplication.setApplicationName("Tribler")
        QCoreApplication.setAttribute(Qt.AA_UseHighDpiPixmaps)

        self.gui_settings = QSettings()
        api_port = api_port or int(get_gui_setting(self.gui_settings, "api_port", DEFAULT_API_PORT))
        dispatcher.update_worker_settings(port=api_port)

        self.navigation_stack = []
        self.tribler_started = False
        self.tribler_settings = None
        self.debug_window = None
        self.core_manager = CoreManager(api_port)
        self.pending_requests = {}
        self.pending_uri_requests = []
        self.download_uri = None
        self.dialog = None
        self.new_version_dialog = None
        self.start_download_dialog_active = False
        self.request_mgr = None
        self.search_request_mgr = None
        self.search_suggestion_mgr = None
        self.selected_torrent_files = []
        self.vlc_available = True
        self.has_search_results = False
        self.last_search_query = None
        self.last_search_time = None
        self.start_time = time.time()
        self.exception_handler_called = False
        self.token_refresh_timer = None
        self.shutdown_timer = None
        self.add_torrent_url_dialog_active = False

        sys.excepthook = self.on_exception

        uic.loadUi(get_ui_file_path('mainwindow.ui'), self)
        TriblerRequestManager.window = self
        self.tribler_status_bar.hide()

        self.token_balance_widget.mouseReleaseEvent = self.on_token_balance_click

        def on_state_update(new_state):
            self.loading_text_label.setText(new_state)

        self.core_manager.core_state_update.connect(on_state_update)

        self.magnet_handler = MagnetHandler(self.window)
        QDesktopServices.setUrlHandler("magnet", self.magnet_handler, "on_open_magnet_link")

        self.debug_pane_shortcut = QShortcut(QKeySequence("Ctrl+d"), self)
        self.debug_pane_shortcut.activated.connect(self.clicked_menu_button_debug)
        self.import_torrent_shortcut = QShortcut(QKeySequence("Ctrl+o"), self)
        self.import_torrent_shortcut.activated.connect(self.on_add_torrent_browse_file)
        self.add_torrent_url_shortcut = QShortcut(QKeySequence("Ctrl+i"), self)
        self.add_torrent_url_shortcut.activated.connect(self.on_add_torrent_from_url)

        # Remove the focus rect on OS X
        for widget in self.findChildren(QLineEdit) + self.findChildren(QListWidget) + self.findChildren(QTreeWidget):
            widget.setAttribute(Qt.WA_MacShowFocusRect, 0)

        self.menu_buttons = [self.left_menu_button_home, self.left_menu_button_search, self.left_menu_button_my_channel,
                             self.left_menu_button_subscriptions, self.left_menu_button_video_player,
                             self.left_menu_button_downloads, self.left_menu_button_discovered]

        self.video_player_page.initialize_player()
        self.search_results_page.initialize_search_results_page(self.gui_settings)
        self.settings_page.initialize_settings_page()
        self.subscribed_channels_page.initialize()
        self.edit_channel_page.initialize_edit_channel_page(self.gui_settings)
        self.downloads_page.initialize_downloads_page()
        self.home_page.initialize_home_page()
        self.loading_page.initialize_loading_page()
        self.discovering_page.initialize_discovering_page()
        self.discovered_page.initialize_discovered_page(self.gui_settings)
        self.channel_page.initialize_channel_page(self.gui_settings)
        self.trust_page.initialize_trust_page()
        self.token_mining_page.initialize_token_mining_page()

        self.stackedWidget.setCurrentIndex(PAGE_LOADING)

        # Create the system tray icon
        if QSystemTrayIcon.isSystemTrayAvailable():
            self.tray_icon = QSystemTrayIcon()
            use_monochrome_icon = get_gui_setting(self.gui_settings, "use_monochrome_icon", False, is_bool=True)
            self.update_tray_icon(use_monochrome_icon)

            # Create the tray icon menu
            menu = self.create_add_torrent_menu()
            show_downloads_action = QAction('Show downloads', self)
            show_downloads_action.triggered.connect(self.clicked_menu_button_downloads)
            token_balance_action = QAction('Show token balance', self)
            token_balance_action.triggered.connect(lambda: self.on_token_balance_click(None))
            quit_action = QAction('Quit Tribler', self)
            quit_action.triggered.connect(self.close_tribler)
            menu.addSeparator()
            menu.addAction(show_downloads_action)
            menu.addAction(token_balance_action)
            menu.addSeparator()
            menu.addAction(quit_action)
            self.tray_icon.setContextMenu(menu)
        else:
            self.tray_icon = None

        self.hide_left_menu_playlist()
        self.left_menu_button_debug.setHidden(True)
        self.top_menu_button.setHidden(True)
        self.left_menu.setHidden(True)
        self.token_balance_widget.setHidden(True)
        self.settings_button.setHidden(True)
        self.add_torrent_button.setHidden(True)
        self.top_search_bar.setHidden(True)

        # Set various icons
        self.top_menu_button.setIcon(QIcon(get_image_path('menu.png')))

        self.search_completion_model = QStringListModel()
        completer = QCompleter()
        completer.setModel(self.search_completion_model)
        completer.setCompletionMode(QCompleter.UnfilteredPopupCompletion)
        self.item_delegate = QStyledItemDelegate()
        completer.popup().setItemDelegate(self.item_delegate)
        completer.popup().setStyleSheet("""
        QListView {
            background-color: #404040;
        }

        QListView::item {
            color: #D0D0D0;
            padding-top: 5px;
            padding-bottom: 5px;
        }

        QListView::item:hover {
            background-color: #707070;
        }
        """)
        self.top_search_bar.setCompleter(completer)

        # Toggle debug if developer mode is enabled
        self.window().left_menu_button_debug.setHidden(
            not get_gui_setting(self.gui_settings, "debug", False, is_bool=True))

        # Start Tribler
        self.core_manager.start(core_args=core_args, core_env=core_env)

        self.core_manager.events_manager.torrent_finished.connect(self.on_torrent_finished)
        self.core_manager.events_manager.new_version_available.connect(self.on_new_version_available)
        self.core_manager.events_manager.tribler_started.connect(self.on_tribler_started)
        self.core_manager.events_manager.events_started.connect(self.on_events_started)
        self.core_manager.events_manager.low_storage_signal.connect(self.on_low_storage)
        self.core_manager.events_manager.credit_mining_signal.connect(self.on_credit_mining_error)
        self.core_manager.events_manager.tribler_shutdown_signal.connect(self.on_tribler_shutdown_state_update)

        self.core_manager.events_manager.upgrader_tick.connect(
            lambda text: self.show_status_bar("Upgrading Tribler database: " + text))
        self.core_manager.events_manager.upgrader_finished.connect(
            lambda _: self.hide_status_bar())

        self.core_manager.events_manager.received_search_result.connect(
            self.search_results_page.received_search_result)

        # Install signal handler for ctrl+c events
        def sigint_handler(*_):
            self.close_tribler()

        signal.signal(signal.SIGINT, sigint_handler)

        self.installEventFilter(self.video_player_page)

        # Resize the window according to the settings
        center = QApplication.desktop().availableGeometry(self).center()
        pos = self.gui_settings.value("pos", QPoint(center.x() - self.width() * 0.5, center.y() - self.height() * 0.5))
        size = self.gui_settings.value("size", self.size())

        self.move(pos)
        self.resize(size)

        self.show()

    def update_tray_icon(self, use_monochrome_icon):
        if not QSystemTrayIcon.isSystemTrayAvailable() or not self.tray_icon:
            return

        if use_monochrome_icon:
            self.tray_icon.setIcon(QIcon(QPixmap(get_image_path('monochrome_tribler.png'))))
        else:
            self.tray_icon.setIcon(QIcon(QPixmap(get_image_path('tribler.png'))))
        self.tray_icon.show()

    def delete_tray_icon(self):
        if self.tray_icon:
            try:
                self.tray_icon.deleteLater()
            except RuntimeError:
                # The tray icon might have already been removed when unloading Qt.
                # This is due to the C code actually being asynchronous.
                logging.debug("Tray icon already removed, no further deletion necessary.")
            self.tray_icon = None

    def on_low_storage(self):
        """
        Dealing with low storage space available. First stop the downloads and the core manager and ask user to user to
        make free space.
        :return:
        """
        self.downloads_page.stop_loading_downloads()
        self.core_manager.stop(False)
        close_dialog = ConfirmationDialog(self.window(), "<b>CRITICAL ERROR</b>",
                                          "You are running low on disk space (<100MB). Please make sure to have "
                                          "sufficient free space available and restart Tribler again.",
                                          [("Close Tribler", BUTTON_TYPE_NORMAL)])
        close_dialog.button_clicked.connect(lambda _: self.close_tribler())
        close_dialog.show()

    def on_torrent_finished(self, torrent_info):
        self.tray_show_message("Download finished", "Download of %s has finished." % torrent_info["name"])

    def show_loading_screen(self):
        self.top_menu_button.setHidden(True)
        self.left_menu.setHidden(True)
        self.token_balance_widget.setHidden(True)
        self.settings_button.setHidden(True)
        self.add_torrent_button.setHidden(True)
        self.top_search_bar.setHidden(True)
        self.stackedWidget.setCurrentIndex(PAGE_LOADING)

    def tray_set_tooltip(self, message):
        """
        Set a tooltip message for the tray icon, if possible.

        :param message: the message to display on hover
        """
        if self.tray_icon:
            try:
                self.tray_icon.setToolTip(message)
            except RuntimeError as e:
                logging.error("Failed to set tray tooltip: %s", str(e))

    def tray_show_message(self, title, message):
        """
        Show a message at the tray icon, if possible.

        :param title: the title of the message
        :param message: the message to display
        """
        if self.tray_icon:
            try:
                self.tray_icon.showMessage(title, message)
            except RuntimeError as e:
                logging.error("Failed to set tray message: %s", str(e))

    def on_tribler_started(self):
        self.tribler_started = True

        self.top_menu_button.setHidden(False)
        self.left_menu.setHidden(False)
        self.token_balance_widget.setHidden(False)
        self.settings_button.setHidden(False)
        self.add_torrent_button.setHidden(False)
        self.top_search_bar.setHidden(False)

        # fetch the settings, needed for the video player port
        self.request_mgr = TriblerRequestManager()
        self.fetch_settings()

        self.downloads_page.start_loading_downloads()
        self.home_page.load_popular_torrents()
        if not self.gui_settings.value("first_discover", False) and not self.core_manager.use_existing_core:
            self.window().gui_settings.setValue("first_discover", True)
            self.discovering_page.is_discovering = True
            self.stackedWidget.setCurrentIndex(PAGE_DISCOVERING)
        else:
            self.clicked_menu_button_home()

        self.setAcceptDrops(True)

    def on_events_started(self, json_dict):
        self.setWindowTitle("Tribler %s" % json_dict["version"])

    def show_status_bar(self, message):
        self.tribler_status_bar_label.setText(message)
        self.tribler_status_bar.show()

    def hide_status_bar(self):
        self.tribler_status_bar.hide()

    def process_uri_request(self):
        """
        Process a URI request if we have one in the queue.
        """
        if len(self.pending_uri_requests) == 0:
            return

        uri = self.pending_uri_requests.pop()
        if uri.startswith('file') or uri.startswith('magnet'):
            self.start_download_from_uri(uri)

    def perform_start_download_request(self, uri, anon_download, safe_seeding, destination, selected_files,
                                       total_files=0, callback=None):
        # Check if destination directory is writable
        is_writable, error = is_dir_writable(destination)
        if not is_writable:
            gui_error_message = "Insufficient write permissions to <i>%s</i> directory. Please add proper " \
                                "write permissions on the directory and add the torrent again. %s" \
                                % (destination, error)
            ConfirmationDialog.show_message(self.window(), "Download error <i>%s</i>" % uri, gui_error_message, "OK")
            return

        selected_files_list = []
        if len(selected_files) != total_files:  # Not all files included
            selected_files_list = [filename for filename in selected_files]

        anon_hops = int(self.tribler_settings['download_defaults']['number_hops']) if anon_download else 0
        safe_seeding = 1 if safe_seeding else 0
        post_data = {
            "uri": uri,
            "anon_hops": anon_hops,
            "safe_seeding": safe_seeding,
            "destination": destination,
            "selected_files": selected_files_list
        }
        request_mgr = TriblerRequestManager()
        request_mgr.perform_request("downloads", callback if callback else self.on_download_added,
                                    method='PUT', data=post_data)

        # Save the download location to the GUI settings
        current_settings = get_gui_setting(self.gui_settings, "recent_download_locations", "")
        recent_locations = current_settings.split(",") if len(current_settings) > 0 else []
        if isinstance(destination, six.text_type):
            destination = destination.encode('utf-8')
        encoded_destination = hexlify(destination)
        if encoded_destination in recent_locations:
            recent_locations.remove(encoded_destination)
        recent_locations.insert(0, encoded_destination)

        if len(recent_locations) > 5:
            recent_locations = recent_locations[:5]

        self.gui_settings.setValue("recent_download_locations", ','.join(recent_locations))

    def on_new_version_available(self, version):
        if version == str(self.gui_settings.value('last_reported_version')):
            return

        self.new_version_dialog = ConfirmationDialog(self, "New version available",
                                                     "Version %s of Tribler is available.Do you want to visit the "
                                                     "website to download the newest version?" % version,
                                                     [('IGNORE', BUTTON_TYPE_NORMAL), ('LATER', BUTTON_TYPE_NORMAL),
                                                      ('OK', BUTTON_TYPE_NORMAL)])
        self.new_version_dialog.button_clicked.connect(lambda action: self.on_new_version_dialog_done(version, action))
        self.new_version_dialog.show()

    def on_new_version_dialog_done(self, version, action):
        if action == 0:  # ignore
            self.gui_settings.setValue("last_reported_version", version)
        elif action == 2:  # ok
            import webbrowser
            webbrowser.open("https://tribler.org")
        if self.new_version_dialog:
            self.new_version_dialog.close_dialog()
            self.new_version_dialog = None

    def on_search_text_change(self, text):
        self.search_suggestion_mgr = TriblerRequestManager()
        self.search_suggestion_mgr.perform_request(
            "search/completions", self.on_received_search_completions, url_params={'q': sanitize_for_fts(text)})

    def on_received_search_completions(self, completions):
        if completions is None:
            return
        self.received_search_completions.emit(completions)
        self.search_completion_model.setStringList(completions["completions"])

    def fetch_settings(self):
        self.request_mgr = TriblerRequestManager()
        self.request_mgr.perform_request("settings", self.received_settings, capture_errors=False)

    def received_settings(self, settings):
        if not settings:
            return
        # If we cannot receive the settings, stop Tribler with an option to send the crash report.
        if 'error' in settings:
            raise RuntimeError(TriblerRequestManager.get_message_from_error(settings))

        self.tribler_settings = settings['settings']

        # Set the video server port
        self.video_player_page.video_player_port = settings["ports"]["video_server~port"]

        # Disable various components based on the settings
        if not self.tribler_settings['video_server']['enabled']:
            self.left_menu_button_video_player.setHidden(True)
        self.downloads_creditmining_button.setHidden(not self.tribler_settings["credit_mining"]["enabled"])
        self.downloads_all_button.click()

        # process pending file requests (i.e. someone clicked a torrent file when Tribler was closed)
        # We do this after receiving the settings so we have the default download location.
        self.process_uri_request()

        # Set token balance refresh timer and load the token balance
        self.token_refresh_timer = QTimer()
        self.token_refresh_timer.timeout.connect(self.load_token_balance)
        self.token_refresh_timer.start(60000)

        self.load_token_balance()

    def on_top_search_button_click(self):
        current_ts = time.time()
        current_search_query = self.top_search_bar.text()

        if self.last_search_query and self.last_search_time \
                and self.last_search_query == self.top_search_bar.text() \
                and current_ts - self.last_search_time < 1:
            logging.info("Same search query already sent within 500ms so dropping this one")
            return

        self.left_menu_button_search.setChecked(True)
        self.has_search_results = True
        self.clicked_menu_button_search()
        self.search_results_page.perform_search(current_search_query)
        self.last_search_query = current_search_query
        self.last_search_time = current_ts

    def on_settings_button_click(self):
        self.deselect_all_menu_buttons()
        self.stackedWidget.setCurrentIndex(PAGE_SETTINGS)
        self.settings_page.load_settings()
        self.navigation_stack = []
        self.hide_left_menu_playlist()

    def on_token_balance_click(self, _):
        self.raise_window()
        self.deselect_all_menu_buttons()
        self.stackedWidget.setCurrentIndex(PAGE_TRUST)
        self.load_token_balance()
        self.trust_page.load_blocks()
        self.navigation_stack = []
        self.hide_left_menu_playlist()

    def load_token_balance(self):
        self.request_mgr = TriblerRequestManager()
        self.request_mgr.perform_request("trustchain/statistics", self.received_trustchain_statistics,
                                         capture_errors=False)

    def received_trustchain_statistics(self, statistics):
        if not statistics or "statistics" not in statistics:
            return

        self.trust_page.received_trustchain_statistics(statistics)

        statistics = statistics["statistics"]
        if 'latest_block' in statistics:
            balance = (statistics["latest_block"]["transaction"]["total_up"] -
                       statistics["latest_block"]["transaction"]["total_down"])
            self.set_token_balance(balance)
        else:
            self.token_balance_label.setText("0 MB")

        # If trust page is currently visible, then load the graph as well
        if self.stackedWidget.currentIndex() == PAGE_TRUST:
            self.trust_page.load_blocks()

    def set_token_balance(self, balance):
        if abs(balance) > 1024 ** 4:  # Balance is over a TB
            balance /= 1024.0 ** 4
            self.token_balance_label.setText("%.1f TB" % balance)
        elif abs(balance) > 1024 ** 3:  # Balance is over a GB
            balance /= 1024.0 ** 3
            self.token_balance_label.setText("%.1f GB" % balance)
        else:
            balance /= 1024.0 ** 2
            self.token_balance_label.setText("%d MB" % balance)

    def raise_window(self):
        self.setWindowState(self.windowState() & ~Qt.WindowMinimized | Qt.WindowActive)
        self.raise_()
        self.activateWindow()

    def create_add_torrent_menu(self):
        """
        Create a menu to add new torrents. Shows when users click on the tray icon or the big plus button.
        """
        menu = TriblerActionMenu(self)

        browse_files_action = QAction('Import torrent from file', self)
        browse_directory_action = QAction('Import torrent(s) from directory', self)
        add_url_action = QAction('Import torrent from magnet/URL', self)
        add_mdblob_action = QAction('Import Tribler metadata from file', self)

        browse_files_action.triggered.connect(self.on_add_torrent_browse_file)
        browse_directory_action.triggered.connect(self.on_add_torrent_browse_dir)
        add_url_action.triggered.connect(self.on_add_torrent_from_url)
        add_mdblob_action.triggered.connect(self.on_add_mdblob_browse_file)

        menu.addAction(browse_files_action)
        menu.addAction(browse_directory_action)
        menu.addAction(add_url_action)
        menu.addAction(add_mdblob_action)

        return menu

    def on_add_torrent_button_click(self, pos):
        self.create_add_torrent_menu().exec_(self.mapToGlobal(self.add_torrent_button.pos()))

    def on_add_torrent_browse_file(self):
        filenames = QFileDialog.getOpenFileNames(self,
                                                 "Please select the .torrent file",
                                                 QDir.homePath(),
                                                 "Torrent files (*.torrent)")
        if len(filenames[0]) > 0:
            [self.pending_uri_requests.append(u"file:%s" % filename) for filename in filenames[0]]
            self.process_uri_request()

    def on_add_mdblob_browse_file(self):
        filenames = QFileDialog.getOpenFileNames(self,
                                                 "Please select the .mdblob file",
                                                 QDir.homePath(),
                                                 "Tribler metadata files (*.mdblob)")
        if len(filenames[0]) > 0:
            for filename in filenames[0]:
                self.pending_uri_requests.append(u"file:%s" % filename)
            self.process_uri_request()

    def start_download_from_uri(self, uri):
        self.download_uri = uri

        if get_gui_setting(self.gui_settings, "ask_download_settings", True, is_bool=True):
            # If tribler settings is not available, fetch the settings and inform the user to try again.
            if not self.tribler_settings:
                self.fetch_settings()
                ConfirmationDialog.show_error(self, "Download Error", "Tribler settings is not available yet. "
                                                                      "Fetching it now. Please try again later.")
                return
            # Clear any previous dialog if exists
            if self.dialog:
                self.dialog.close_dialog()
                self.dialog = None

            self.dialog = StartDownloadDialog(self, self.download_uri)
            self.dialog.button_clicked.connect(self.on_start_download_action)
            self.dialog.show()
            self.start_download_dialog_active = True
        else:
            # In the unlikely scenario that tribler settings are not available yet, try to fetch settings again and
            # add the download uri back to self.pending_uri_requests to process again.
            if not self.tribler_settings:
                self.fetch_settings()
                if self.download_uri not in self.pending_uri_requests:
                    self.pending_uri_requests.append(self.download_uri)
                return

            self.window().perform_start_download_request(self.download_uri,
                                                         self.window().tribler_settings['download_defaults'][
                                                             'anonymity_enabled'],
                                                         self.window().tribler_settings['download_defaults'][
                                                             'safeseeding_enabled'],
                                                         self.tribler_settings['download_defaults']['saveas'], [], 0)
            self.process_uri_request()

    def on_start_download_action(self, action):
        if action == 1:
            if self.dialog and self.dialog.dialog_widget:
                self.window().perform_start_download_request(
                    self.download_uri, self.dialog.dialog_widget.anon_download_checkbox.isChecked(),
                    self.dialog.dialog_widget.safe_seed_checkbox.isChecked(),
                    self.dialog.dialog_widget.destination_input.currentText(),
                    self.dialog.get_selected_files(),
                    self.dialog.dialog_widget.files_list_view.topLevelItemCount())
            else:
                ConfirmationDialog.show_error(self, "Tribler UI Error", "Something went wrong. Please try again.")
                logging.exception("Error while trying to download. Either dialog or dialog.dialog_widget is None")

        if self.dialog:
            self.dialog.close_dialog()
            self.dialog = None
            self.start_download_dialog_active = False

        if action == 0:  # We do this after removing the dialog since process_uri_request is blocking
            self.process_uri_request()

    def on_add_torrent_browse_dir(self):
        chosen_dir = QFileDialog.getExistingDirectory(self,
                                                      "Please select the directory containing the .torrent files",
                                                      QDir.homePath(),
                                                      QFileDialog.ShowDirsOnly)

        if len(chosen_dir) != 0:
            self.selected_torrent_files = [torrent_file for torrent_file in glob.glob(chosen_dir + "/*.torrent")]
            self.dialog = ConfirmationDialog(self, "Add torrents from directory",
                                             "Are you sure you want to add %d torrents to Tribler?" %
                                             len(self.selected_torrent_files),
                                             [('ADD', BUTTON_TYPE_NORMAL), ('CANCEL', BUTTON_TYPE_CONFIRM)])
            self.dialog.button_clicked.connect(self.on_confirm_add_directory_dialog)
            self.dialog.show()

    def on_confirm_add_directory_dialog(self, action):
        if action == 0:
            for torrent_file in self.selected_torrent_files:
                escaped_uri = u"file:%s" % pathname2url(torrent_file.encode('utf-8'))
                self.perform_start_download_request(escaped_uri,
                                                    self.window().tribler_settings['download_defaults'][
                                                        'anonymity_enabled'],
                                                    self.window().tribler_settings['download_defaults'][
                                                        'safeseeding_enabled'],
                                                    self.tribler_settings['download_defaults']['saveas'], [], 0)

        if self.dialog:
            self.dialog.close_dialog()
            self.dialog = None

    def on_add_torrent_from_url(self):
        # Make sure that the window is visible (this action might be triggered from the tray icon)
        self.raise_window()

        if self.video_player_page.isVisible():
            # If we're adding a torrent from the video player page, go to the home page.
            # This is necessary since VLC takes the screen and the popup becomes invisible.
            self.clicked_menu_button_home()

        if not self.add_torrent_url_dialog_active:
            self.dialog = ConfirmationDialog(self, "Add torrent from URL/magnet link",
                                             "Please enter the URL/magnet link in the field below:",
                                             [('ADD', BUTTON_TYPE_NORMAL), ('CANCEL', BUTTON_TYPE_CONFIRM)],
                                             show_input=True)
            self.dialog.dialog_widget.dialog_input.setPlaceholderText('URL/magnet link')
            self.dialog.dialog_widget.dialog_input.setFocus()
            self.dialog.button_clicked.connect(self.on_torrent_from_url_dialog_done)
            self.dialog.show()
            self.add_torrent_url_dialog_active = True

    def on_torrent_from_url_dialog_done(self, action):
        self.add_torrent_url_dialog_active = False
        if self.dialog and self.dialog.dialog_widget:
            uri = self.dialog.dialog_widget.dialog_input.text().strip()

            # If the URI is a 40-bytes hex-encoded infohash, convert it to a valid magnet link
            if len(uri) == 40:
                valid_ih_hex = True
                try:
                    int(uri, 16)
                except ValueError:
                    valid_ih_hex = False

                if valid_ih_hex:
                    uri = "magnet:?xt=urn:btih:" + uri

            # Remove first dialog
            self.dialog.close_dialog()
            self.dialog = None

            if action == 0:
                self.start_download_from_uri(uri)

    def on_download_added(self, result):
        if not result:
            return
        if len(self.pending_uri_requests) == 0:  # Otherwise, we first process the remaining requests.
            self.window().left_menu_button_downloads.click()
        else:
            self.process_uri_request()

    def on_top_menu_button_click(self):
        if self.left_menu.isHidden():
            self.left_menu.show()
        else:
            self.left_menu.hide()

    def deselect_all_menu_buttons(self, except_select=None):
        for button in self.menu_buttons:
            if button == except_select:
                button.setEnabled(False)
                continue
            button.setEnabled(True)

            if button == self.left_menu_button_search and not self.has_search_results:
                button.setEnabled(False)

            button.setChecked(False)

    def clicked_menu_button_home(self):
        self.deselect_all_menu_buttons(self.left_menu_button_home)
        self.stackedWidget.setCurrentIndex(PAGE_HOME)
        self.navigation_stack = []
        self.hide_left_menu_playlist()

    def clicked_menu_button_search(self):
        self.deselect_all_menu_buttons(self.left_menu_button_search)
        self.stackedWidget.setCurrentIndex(PAGE_SEARCH_RESULTS)
        self.navigation_stack = []
        self.hide_left_menu_playlist()

    def clicked_menu_button_discovered(self):
        self.deselect_all_menu_buttons(self.left_menu_button_discovered)
        self.stackedWidget.setCurrentIndex(PAGE_DISCOVERED)
        self.discovered_page.load_discovered_channels()
        self.discovered_channels_list.setFocus()
        self.navigation_stack = []
        self.hide_left_menu_playlist()

    def clicked_menu_button_my_channel(self):
        self.deselect_all_menu_buttons(self.left_menu_button_my_channel)
        self.stackedWidget.setCurrentIndex(PAGE_EDIT_CHANNEL)
        self.edit_channel_page.load_my_channel_overview()
        self.navigation_stack = []
        self.hide_left_menu_playlist()

    def clicked_menu_button_video_player(self):
        self.deselect_all_menu_buttons(self.left_menu_button_video_player)
        self.stackedWidget.setCurrentIndex(PAGE_VIDEO_PLAYER)
        self.navigation_stack = []
        self.show_left_menu_playlist()

    def clicked_menu_button_downloads(self):
        self.deselect_all_menu_buttons(self.left_menu_button_downloads)
        self.raise_window()
        self.left_menu_button_downloads.setChecked(True)
        self.stackedWidget.setCurrentIndex(PAGE_DOWNLOADS)
        self.navigation_stack = []
        self.hide_left_menu_playlist()

    def clicked_menu_button_debug(self):
        if not self.debug_window:
            self.debug_window = DebugWindow(self.tribler_settings, self.core_manager.events_manager.tribler_version)
        self.debug_window.show()

    def clicked_menu_button_subscriptions(self):
        self.deselect_all_menu_buttons(self.left_menu_button_subscriptions)
        self.stackedWidget.setCurrentIndex(PAGE_SUBSCRIBED_CHANNELS)
        self.subscribed_channels_page.load_subscribed_channels()
        self.navigation_stack = []
        self.hide_left_menu_playlist()

    def hide_left_menu_playlist(self):
        self.left_menu_seperator.setHidden(True)
        self.left_menu_playlist_label.setHidden(True)
        self.left_menu_playlist.setHidden(True)

    def show_left_menu_playlist(self):
        self.left_menu_seperator.setHidden(False)
        self.left_menu_playlist_label.setHidden(False)
        self.left_menu_playlist.setHidden(False)

    def on_channel_clicked(self, channel_info):
        self.channel_page.initialize_with_channel(channel_info)
        self.navigation_stack.append(self.stackedWidget.currentIndex())
        self.stackedWidget.setCurrentIndex(PAGE_CHANNEL_DETAILS)

    def on_page_back_clicked(self):
        try:
            prev_page = self.navigation_stack.pop()
            self.stackedWidget.setCurrentIndex(prev_page)
        except IndexError:
            logging.exception("Unknown page found in stack")

    def on_credit_mining_error(self, error):
        ConfirmationDialog.show_error(self, "Credit Mining Error", error[u'message'])

    def on_edit_channel_clicked(self):
        self.stackedWidget.setCurrentIndex(PAGE_EDIT_CHANNEL)
        self.navigation_stack = []
        self.channel_page.on_edit_channel_clicked()

    def resizeEvent(self, _):
        # Resize home page cells
        cell_width = self.home_page_table_view.width() / 3 - 3  # We have some padding to the right
        max_height = self.home_page_table_view.height() / 3 - 4
        cell_height = min(cell_width / 2 + 60, max_height)

        for i in range(0, 3):
            self.home_page_table_view.setColumnWidth(i, cell_width)
            self.home_page_table_view.setRowHeight(i, cell_height)
        self.resize_event.emit()

    def exit_full_screen(self):
        self.top_bar.show()
        self.left_menu.show()
        self.video_player_page.is_full_screen = False
        self.showNormal()

    def close_tribler(self):
        if not self.core_manager.shutting_down:
            def show_force_shutdown():
                self.window().force_shutdown_btn.show()

            self.delete_tray_icon()
            self.show_loading_screen()
            self.hide_status_bar()
            self.loading_text_label.setText("Shutting down...")
            if self.debug_window:
                self.debug_window.setHidden(True)

            self.shutdown_timer = QTimer()
            self.shutdown_timer.timeout.connect(show_force_shutdown)
            self.shutdown_timer.start(SHUTDOWN_WAITING_PERIOD)

            self.gui_settings.setValue("pos", self.pos())
            self.gui_settings.setValue("size", self.size())

            if self.core_manager.use_existing_core:
                # Don't close the core that we are using
                QApplication.quit()

            self.core_manager.stop()
            self.core_manager.shutting_down = True
            self.downloads_page.stop_loading_downloads()
            request_queue.clear()

            # Stop the token balance timer
            if self.token_refresh_timer:
                self.token_refresh_timer.stop()

    def closeEvent(self, close_event):
        self.close_tribler()
        close_event.ignore()

    def keyReleaseEvent(self, event):
        if event.key() == Qt.Key_Escape:
            self.escape_pressed.emit()
            if self.isFullScreen():
                self.exit_full_screen()

    def dragEnterEvent(self, e):
        file_urls = [_qurl_to_path(url) for url in e.mimeData().urls()] if e.mimeData().hasUrls() else []

        if any(os.path.isfile(filename) for filename in file_urls):
            e.accept()
        else:
            e.ignore()

    def dropEvent(self, e):
        file_urls = ([(_qurl_to_path(url), url.toString()) for url in e.mimeData().urls()]
                     if e.mimeData().hasUrls() else [])

        for filename, fileurl in file_urls:
            if os.path.isfile(filename):
                self.start_download_from_uri(fileurl)

        e.accept()

    def clicked_force_shutdown(self):
        process_checker = ProcessChecker()
        if process_checker.already_running:
            core_pid = process_checker.get_pid_from_lock_file()
            os.kill(int(core_pid), 9)
        # Stop the Qt application
        QApplication.quit()

    def on_tribler_shutdown_state_update(self, state):
        self.loading_text_label.setText(state)
Beispiel #14
0
class XNova_MainWindow(QWidget):

    STATE_NOT_AUTHED = 0
    STATE_AUTHED = 1

    def __init__(self, parent=None):
        super(XNova_MainWindow, self).__init__(parent, Qt.Window)
        # state vars
        self.config_store_dir = './cache'
        self.cfg = configparser.ConfigParser()
        self.cfg.read('config/net.ini', encoding='utf-8')
        self.state = self.STATE_NOT_AUTHED
        self.login_email = ''
        self.cookies_dict = {}
        self._hidden_to_tray = False
        #
        # init UI
        self.setWindowIcon(QIcon(':/i/xnova_logo_64.png'))
        self.setWindowTitle('XNova Commander')
        # main layouts
        self._layout = QVBoxLayout()
        self._layout.setContentsMargins(0, 2, 0, 0)
        self._layout.setSpacing(3)
        self.setLayout(self._layout)
        self._horizontal_layout = QHBoxLayout()
        self._horizontal_layout.setContentsMargins(0, 0, 0, 0)
        self._horizontal_layout.setSpacing(6)
        # flights frame
        self._fr_flights = QFrame(self)
        self._fr_flights.setMinimumHeight(22)
        self._fr_flights.setFrameShape(QFrame.NoFrame)
        self._fr_flights.setFrameShadow(QFrame.Plain)
        # planets bar scrollarea
        self._sa_planets = QScrollArea(self)
        self._sa_planets.setMinimumWidth(125)
        self._sa_planets.setMaximumWidth(125)
        self._sa_planets.setFrameShape(QFrame.NoFrame)
        self._sa_planets.setFrameShadow(QFrame.Plain)
        self._sa_planets.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
        self._sa_planets.setWidgetResizable(True)
        self._panel_planets = QWidget(self._sa_planets)
        self._layout_pp = QVBoxLayout()
        self._panel_planets.setLayout(self._layout_pp)
        self._lbl_planets = QLabel(self.tr('Planets:'), self._panel_planets)
        self._lbl_planets.setMaximumHeight(32)
        self._layout_pp.addWidget(self._lbl_planets)
        self._layout_pp.addStretch()
        self._sa_planets.setWidget(self._panel_planets)
        #
        # tab widget
        self._tabwidget = XTabWidget(self)
        self._tabwidget.enableButtonAdd(False)
        self._tabwidget.tabCloseRequested.connect(self.on_tab_close_requested)
        self._tabwidget.addClicked.connect(self.on_tab_add_clicked)
        #
        # create status bar
        self._statusbar = XNCStatusBar(self)
        self.set_status_message(self.tr('Not connected: Log in!'))
        #
        # tab widget pages
        self.login_widget = None
        self.flights_widget = None
        self.overview_widget = None
        self.imperium_widget = None
        #
        # settings widget
        self.settings_widget = SettingsWidget(self)
        self.settings_widget.settings_changed.connect(self.on_settings_changed)
        self.settings_widget.hide()
        #
        # finalize layouts
        self._horizontal_layout.addWidget(self._sa_planets)
        self._horizontal_layout.addWidget(self._tabwidget)
        self._layout.addWidget(self._fr_flights)
        self._layout.addLayout(self._horizontal_layout)
        self._layout.addWidget(self._statusbar)
        #
        # system tray icon
        self.tray_icon = None
        show_tray_icon = False
        if 'tray' in self.cfg:
            if (self.cfg['tray']['icon_usage'] == 'show') or \
                    (self.cfg['tray']['icon_usage'] == 'show_min'):
                self.create_tray_icon()
        #
        # try to restore last window size
        ssz = self.load_cfg_val('main_size')
        if ssz is not None:
            self.resize(ssz[0], ssz[1])
        #
        # world initialization
        self.world = XNovaWorld_instance()
        self.world_timer = QTimer(self)
        self.world_timer.timeout.connect(self.on_world_timer)

    # overrides QWidget.closeEvent
    # cleanup just before the window close
    def closeEvent(self, close_event: QCloseEvent):
        logger.debug('closing')
        if self.tray_icon is not None:
            self.tray_icon.hide()
            self.tray_icon = None
        if self.world_timer.isActive():
            self.world_timer.stop()
        self.world.script_command = 'stop'  # also stop possible running scripts
        if self.world.isRunning():
            self.world.quit()
            logger.debug('waiting for world thread to stop (5 sec)...')
            wait_res = self.world.wait(5000)
            if not wait_res:
                logger.warn('wait failed, last chance, terminating!')
                self.world.terminate()
        # store window size
        ssz = (self.width(), self.height())
        self.store_cfg_val('main_size', ssz)
        # accept the event
        close_event.accept()

    def showEvent(self, evt: QShowEvent):
        super(XNova_MainWindow, self).showEvent(evt)
        self._hidden_to_tray = False

    def changeEvent(self, evt: QEvent):
        super(XNova_MainWindow, self).changeEvent(evt)
        if evt.type() == QEvent.WindowStateChange:
            if not isinstance(evt, QWindowStateChangeEvent):
                return
            # make sure we only do this for minimize events
            if (evt.oldState() != Qt.WindowMinimized) and self.isMinimized():
                # we were minimized! explicitly hide settings widget
                #     if it is open, otherwise it will be lost forever :(
                if self.settings_widget is not None:
                    if self.settings_widget.isVisible():
                        self.settings_widget.hide()
                # should we minimize to tray?
                if self.cfg['tray']['icon_usage'] == 'show_min':
                    if not self._hidden_to_tray:
                        self._hidden_to_tray = True
                        self.hide()

    def create_tray_icon(self):
        if QSystemTrayIcon.isSystemTrayAvailable():
            logger.debug('System tray icon is available, showing')
            self.tray_icon = QSystemTrayIcon(QIcon(':/i/xnova_logo_32.png'), self)
            self.tray_icon.setToolTip(self.tr('XNova Commander'))
            self.tray_icon.activated.connect(self.on_tray_icon_activated)
            self.tray_icon.show()
        else:
            self.tray_icon = None

    def hide_tray_icon(self):
        if self.tray_icon is not None:
            self.tray_icon.hide()
            self.tray_icon.deleteLater()
            self.tray_icon = None

    def set_tray_tooltip(self, tip: str):
        if self.tray_icon is not None:
            self.tray_icon.setToolTip(tip)

    def set_status_message(self, msg: str):
        self._statusbar.set_status(msg)

    def store_cfg_val(self, category: str, value):
        pickle_filename = '{0}/{1}.dat'.format(self.config_store_dir, category)
        try:
            cache_dir = pathlib.Path(self.config_store_dir)
            if not cache_dir.exists():
                cache_dir.mkdir()
            with open(pickle_filename, 'wb') as f:
                pickle.dump(value, f)
        except pickle.PickleError as pe:
            pass
        except IOError as ioe:
            pass

    def load_cfg_val(self, category: str, default_value=None):
        value = None
        pickle_filename = '{0}/{1}.dat'.format(self.config_store_dir, category)
        try:
            with open(pickle_filename, 'rb') as f:
                value = pickle.load(f)
                if value is None:
                    value = default_value
        except pickle.PickleError as pe:
            pass
        except IOError as ioe:
            pass
        return value

    @pyqtSlot()
    def on_settings_changed(self):
        self.cfg.read('config/net.ini', encoding='utf-8')
        # maybe show/hide tray icon now?
        show_tray_icon = False
        if 'tray' in self.cfg:
            icon_usage = self.cfg['tray']['icon_usage']
            if (icon_usage == 'show') or (icon_usage == 'show_min'):
                show_tray_icon = True
        # show if needs show and hidden, or hide if shown and needs to hide
        if show_tray_icon and (self.tray_icon is None):
            logger.debug('settings changed, showing tray icon')
            self.create_tray_icon()
        elif (not show_tray_icon) and (self.tray_icon is not None):
            logger.debug('settings changed, hiding tray icon')
            self.hide_tray_icon()
        # also notify world about changed config!
        self.world.reload_config()

    def add_tab(self, widget: QWidget, title: str, closeable: bool = True) -> int:
        tab_index = self._tabwidget.addTab(widget, title, closeable)
        return tab_index

    def remove_tab(self, index: int):
        self._tabwidget.removeTab(index)

    # called by main application object just after main window creation
    # to show login widget and begin login process
    def begin_login(self):
        # create flights widget
        self.flights_widget = FlightsWidget(self._fr_flights)
        self.flights_widget.load_ui()
        install_layout_for_widget(self._fr_flights, Qt.Vertical, margins=(1, 1, 1, 1), spacing=1)
        self._fr_flights.layout().addWidget(self.flights_widget)
        self.flights_widget.set_online_state(False)
        self.flights_widget.requestShowSettings.connect(self.on_show_settings)
        # create and show login widget as first tab
        self.login_widget = LoginWidget(self._tabwidget)
        self.login_widget.load_ui()
        self.login_widget.loginError.connect(self.on_login_error)
        self.login_widget.loginOk.connect(self.on_login_ok)
        self.login_widget.show()
        self.add_tab(self.login_widget, self.tr('Login'), closeable=False)
        # self.test_setup_planets_panel()
        # self.test_planet_tab()

    def setup_planets_panel(self, planets: list):
        layout = self._panel_planets.layout()
        layout.setSpacing(0)
        remove_trailing_spacer_from_layout(layout)
        # remove all previous planet widgets from planets panel
        if layout.count() > 0:
            for i in range(layout.count()-1, -1, -1):
                li = layout.itemAt(i)
                if li is not None:
                    wi = li.widget()
                    if wi is not None:
                        if isinstance(wi, PlanetSidebarWidget):
                            layout.removeWidget(wi)
                            wi.close()
                            wi.deleteLater()  # fix possible mem leak
                            del wi
        for pl in planets:
            pw = PlanetSidebarWidget(self._panel_planets)
            pw.setPlanet(pl)
            layout.addWidget(pw)
            pw.show()
            # connections from each planet bar widget
            pw.requestOpenGalaxy.connect(self.on_request_open_galaxy_tab)
            pw.requestOpenPlanet.connect(self.on_request_open_planet_tab)
        append_trailing_spacer_to_layout(layout)

    def update_planets_panel(self):
        """
        Calls QWidget.update() on every PlanetBarWidget
        embedded in ui.panel_planets, causing repaint
        """
        layout = self._panel_planets.layout()
        if layout.count() > 0:
            for i in range(layout.count()):
                li = layout.itemAt(i)
                if li is not None:
                    wi = li.widget()
                    if wi is not None:
                        if isinstance(wi, PlanetSidebarWidget):
                            wi.update()

    def add_tab_for_planet(self, planet: XNPlanet):
        # construct planet widget and setup signals/slots
        plw = PlanetWidget(self._tabwidget)
        plw.requestOpenGalaxy.connect(self.on_request_open_galaxy_tab)
        plw.setPlanet(planet)
        # construct tab title
        tab_title = '{0} {1}'.format(planet.name, planet.coords.coords_str())
        # add tab and make it current
        tab_index = self.add_tab(plw, tab_title, closeable=True)
        self._tabwidget.setCurrentIndex(tab_index)
        self._tabwidget.tabBar().setTabIcon(tab_index, QIcon(':/i/planet_32.png'))
        return tab_index

    def add_tab_for_galaxy(self, coords: XNCoords = None):
        gw = GalaxyWidget(self._tabwidget)
        tab_title = '{0}'.format(self.tr('Galaxy'))
        if coords is not None:
            tab_title = '{0} {1}'.format(self.tr('Galaxy'), coords.coords_str())
            gw.setCoords(coords.galaxy, coords.system)
        idx = self.add_tab(gw, tab_title, closeable=True)
        self._tabwidget.setCurrentIndex(idx)
        self._tabwidget.tabBar().setTabIcon(idx, QIcon(':/i/galaxy_32.png'))

    @pyqtSlot(int)
    def on_tab_close_requested(self, idx: int):
        # logger.debug('tab close requested: {0}'.format(idx))
        if idx <= 1:  # cannot close overview or imperium tabs
            return
        self.remove_tab(idx)

    @pyqtSlot()
    def on_tab_add_clicked(self):
        pos = QCursor.pos()
        planets = self.world.get_planets()
        # logger.debug('tab bar add clicked, cursor pos = ({0}, {1})'.format(pos.x(), pos.y()))
        menu = QMenu(self)
        # galaxy view
        galaxy_action = QAction(menu)
        galaxy_action.setText(self.tr('Add galaxy view'))
        galaxy_action.setData(QVariant('galaxy'))
        menu.addAction(galaxy_action)
        # planets
        menu.addSection(self.tr('-- Planet tabs: --'))
        for planet in planets:
            action = QAction(menu)
            action.setText('{0} {1}'.format(planet.name, planet.coords.coords_str()))
            action.setData(QVariant(planet.planet_id))
            menu.addAction(action)
        action_ret = menu.exec(pos)
        if action_ret is not None:
            # logger.debug('selected action data = {0}'.format(str(action_ret.data())))
            if action_ret == galaxy_action:
                logger.debug('action_ret == galaxy_action')
                self.add_tab_for_galaxy()
                return
            # else consider this is planet widget
            planet_id = int(action_ret.data())
            self.on_request_open_planet_tab(planet_id)

    @pyqtSlot(str)
    def on_login_error(self, errstr):
        logger.error('Login error: {0}'.format(errstr))
        self.state = self.STATE_NOT_AUTHED
        self.set_status_message(self.tr('Login error: {0}').format(errstr))
        QMessageBox.critical(self, self.tr('Login error:'), errstr)

    @pyqtSlot(str, dict)
    def on_login_ok(self, login_email, cookies_dict):
        # logger.debug('Login OK, login: {0}, cookies: {1}'.format(login_email, str(cookies_dict)))
        # save login data: email, cookies
        self.state = self.STATE_AUTHED
        self.set_status_message(self.tr('Login OK, loading world'))
        self.login_email = login_email
        self.cookies_dict = cookies_dict
        #
        # destroy login widget and remove its tab
        self.remove_tab(0)
        self.login_widget.close()
        self.login_widget.deleteLater()
        self.login_widget = None
        #
        # create overview widget and add it as first tab
        self.overview_widget = OverviewWidget(self._tabwidget)
        self.overview_widget.load_ui()
        self.add_tab(self.overview_widget, self.tr('Overview'), closeable=False)
        self.overview_widget.show()
        self.overview_widget.setEnabled(False)
        #
        # create 2nd tab - Imperium
        self.imperium_widget = ImperiumWidget(self._tabwidget)
        self.add_tab(self.imperium_widget, self.tr('Imperium'), closeable=False)
        self.imperium_widget.setEnabled(False)
        #
        # initialize XNova world updater
        self.world.initialize(cookies_dict)
        self.world.set_login_email(self.login_email)
        # connect signals from world
        self.world.world_load_progress.connect(self.on_world_load_progress)
        self.world.world_load_complete.connect(self.on_world_load_complete)
        self.world.net_request_started.connect(self.on_net_request_started)
        self.world.net_request_finished.connect(self.on_net_request_finished)
        self.world.flight_arrived.connect(self.on_flight_arrived)
        self.world.build_complete.connect(self.on_building_complete)
        self.world.loaded_overview.connect(self.on_loaded_overview)
        self.world.loaded_imperium.connect(self.on_loaded_imperium)
        self.world.loaded_planet.connect(self.on_loaded_planet)
        self.world.start()

    @pyqtSlot(str, int)
    def on_world_load_progress(self, comment: str, progress: int):
        self._statusbar.set_world_load_progress(comment, progress)

    @pyqtSlot()
    def on_world_load_complete(self):
        logger.debug('main: on_world_load_complete()')
        # enable adding new tabs
        self._tabwidget.enableButtonAdd(True)
        # update statusbar
        self._statusbar.set_world_load_progress('', -1)  # turn off progress display
        self.set_status_message(self.tr('World loaded.'))
        # update account info
        if self.overview_widget is not None:
            self.overview_widget.setEnabled(True)
            self.overview_widget.update_account_info()
            self.overview_widget.update_builds()
        # update flying fleets
        self.flights_widget.set_online_state(True)
        self.flights_widget.update_flights()
        # update planets
        planets = self.world.get_planets()
        self.setup_planets_panel(planets)
        if self.imperium_widget is not None:
            self.imperium_widget.setEnabled(True)
            self.imperium_widget.update_planets()
        # update statusbar
        self._statusbar.update_online_players_count()
        # update tray tooltip, add account name
        self.set_tray_tooltip(self.tr('XNova Commander') + ' - '
                              + self.world.get_account_info().login)
        # set timer to do every-second world recalculation
        self.world_timer.setInterval(1000)
        self.world_timer.setSingleShot(False)
        self.world_timer.start()

    @pyqtSlot()
    def on_loaded_overview(self):
        logger.debug('on_loaded_overview')
        # A lot of things are updated when overview is loaded
        #  * Account information and stats
        if self.overview_widget is not None:
            self.overview_widget.update_account_info()
        #  * flights will be updated every second anyway in on_world_timer(), so no need to call
        #    self.flights_widget.update_flights()
        #  * messages count also, is updated with flights
        #  * current planet may have changed
        self.update_planets_panel()
        #  * server time is updated also
        self._statusbar.update_online_players_count()

    @pyqtSlot()
    def on_loaded_imperium(self):
        logger.debug('on_loaded_imperium')
        # need to update imperium widget
        if self.imperium_widget is not None:
            self.imperium_widget.update_planets()
        # The important note here is that imperium update is the only place where
        # the planets list is read, so number of planets, their names, etc may change here
        # Also, imperium update OVERWRITES full planets array, so, all prev
        # references to planets in all GUI elements must be invalidated, because
        # they will point to unused, outdated planets
        planets = self.world.get_planets()
        # re-create planets sidebar
        self.setup_planets_panel(planets)
        # update all builds in overview widget
        if self.overview_widget:
            self.overview_widget.update_builds()
        # update all planet tabs with new planet references
        cnt = self._tabwidget.count()
        if cnt > 2:
            for index in range(2, cnt):
                tab_page = self._tabwidget.tabWidget(index)
                if tab_page is not None:
                    try:
                        tab_type = tab_page.get_tab_type()
                        if tab_type == 'planet':
                            tab_planet = tab_page.planet()
                            new_planet = self.world.get_planet(tab_planet.planet_id)
                            tab_page.setPlanet(new_planet)
                    except AttributeError:  # not all pages may have method get_tab_type()
                        pass

    @pyqtSlot(int)
    def on_loaded_planet(self, planet_id: int):
        logger.debug('Got signal on_loaded_planet({0}), updating overview '
                     'widget and planets panel'.format(planet_id))
        if self.overview_widget:
            self.overview_widget.update_builds()
        self.update_planets_panel()
        # update also planet tab, if any
        planet = self.world.get_planet(planet_id)
        if planet is not None:
            tab_idx = self.find_tab_for_planet(planet_id)
            if tab_idx != -1:
                tab_widget = self._tabwidget.tabWidget(tab_idx)
                if isinstance(tab_widget, PlanetWidget):
                    logger.debug('Updating planet tab #{}'.format(tab_idx))
                    tab_widget.setPlanet(planet)

    @pyqtSlot()
    def on_world_timer(self):
        if self.world:
            self.world.world_tick()
        self.update_planets_panel()
        if self.flights_widget:
            self.flights_widget.update_flights()
        if self.overview_widget:
            self.overview_widget.update_builds()
        if self.imperium_widget:
            self.imperium_widget.update_planet_resources()

    @pyqtSlot()
    def on_net_request_started(self):
        self._statusbar.set_loading_status(True)

    @pyqtSlot()
    def on_net_request_finished(self):
        self._statusbar.set_loading_status(False)

    @pyqtSlot(int)
    def on_tray_icon_activated(self, reason):
        # QSystemTrayIcon::Unknown	0	Unknown reason
        # QSystemTrayIcon::Context	1	The context menu for the system tray entry was requested
        # QSystemTrayIcon::DoubleClick	2	The system tray entry was double clicked
        # QSystemTrayIcon::Trigger	3	The system tray entry was clicked
        # QSystemTrayIcon::MiddleClick	4	The system tray entry was clicked with the middle mouse button
        if reason == QSystemTrayIcon.Trigger:
            # left-click
            self.setWindowState((self.windowState() & ~Qt.WindowMinimized) | Qt.WindowActive)
            self.show()
            return

    def show_tray_message(self, title, message, icon_type=None, timeout_ms=None):
        """
        Shows message from system tray icon, if system supports it.
        If no support, this is just a no-op
        :param title: message title
        :param message: message text
        :param icon_type: one of:
        QSystemTrayIcon.NoIcon      0   No icon is shown.
        QSystemTrayIcon.Information 1   An information icon is shown.
        QSystemTrayIcon.Warning     2   A standard warning icon is shown.
        QSystemTrayIcon.Critical    3   A critical warning icon is shown
        """
        if self.tray_icon is None:
            return
        if self.tray_icon.supportsMessages():
            if icon_type is None:
                icon_type = QSystemTrayIcon.Information
            if timeout_ms is None:
                timeout_ms = 10000
            self.tray_icon.showMessage(title, message, icon_type, timeout_ms)
        else:
            logger.info('This system does not support tray icon messages.')

    @pyqtSlot()
    def on_show_settings(self):
        if self.settings_widget is not None:
            self.settings_widget.show()
            self.settings_widget.showNormal()

    @pyqtSlot(XNFlight)
    def on_flight_arrived(self, fl: XNFlight):
        logger.debug('main: flight arrival: {0}'.format(fl))
        mis_str = flight_mission_for_humans(fl.mission)
        if fl.direction == 'return':
            mis_str += ' ' + self.tr('return')
        short_fleet_info = self.tr('{0} {1} => {2}, {3} ship(s)').format(
            mis_str, fl.src, fl.dst, len(fl.ships))
        self.show_tray_message(self.tr('XNova: Fleet arrived'), short_fleet_info)

    @pyqtSlot(XNPlanet, XNPlanetBuildingItem)
    def on_building_complete(self, planet: XNPlanet, bitem: XNPlanetBuildingItem):
        logger.debug('main: build complete: on planet {0}: {1}'.format(
            planet.name, str(bitem)))
        # update also planet tab, if any
        if isinstance(planet, XNPlanet):
            tab_idx = self.find_tab_for_planet(planet.planet_id)
            if tab_idx != -1:
                tab_widget = self._tabwidget.tabWidget(tab_idx)
                if isinstance(tab_widget, PlanetWidget):
                    logger.debug('Updating planet tab #{}'.format(tab_idx))
                    tab_widget.setPlanet(planet)
        # construct message to show in tray
        if bitem.is_shipyard_item:
            binfo_str = '{0} x {1}'.format(bitem.quantity, bitem.name)
        else:
            binfo_str = self.tr('{0} lv.{1}').format(bitem.name, bitem.level)
        msg = self.tr('{0} has built {1}').format(planet.name, binfo_str)
        self.show_tray_message(self.tr('XNova: Building complete'), msg)

    @pyqtSlot(XNCoords)
    def on_request_open_galaxy_tab(self, coords: XNCoords):
        tab_index = self.find_tab_for_galaxy(coords.galaxy, coords.system)
        if tab_index == -1:  # create new tab for these coords
            self.add_tab_for_galaxy(coords)
            return
        # else switch to that tab
        self._tabwidget.setCurrentIndex(tab_index)

    @pyqtSlot(int)
    def on_request_open_planet_tab(self, planet_id: int):
        tab_index = self.find_tab_for_planet(planet_id)
        if tab_index == -1: # create new tab for planet
            planet = self.world.get_planet(planet_id)
            if planet is not None:
                self.add_tab_for_planet(planet)
                return
        # else switch to that tab
        self._tabwidget.setCurrentIndex(tab_index)

    def find_tab_for_planet(self, planet_id: int) -> int:
        """
        Finds tab index where specified planet is already opened
        :param planet_id: planet id to search for
        :return: tab index, or -1 if not found
        """
        cnt = self._tabwidget.count()
        if cnt < 3:
            return -1  # only overview and imperium tabs are present
        for index in range(2, cnt):
            tab_page = self._tabwidget.tabWidget(index)
            if tab_page is not None:
                try:
                    tab_type = tab_page.get_tab_type()
                    if tab_type == 'planet':
                        tab_planet = tab_page.planet()
                        if tab_planet.planet_id == planet_id:
                            # we have found tab index where this planet is already opened
                            return index
                except AttributeError:  # not all pages may have method get_tab_type()
                    pass
        return -1

    def find_tab_for_galaxy(self, galaxy: int, system: int) -> int:
        """
        Finds tab index where specified galaxy view is already opened
        :param galaxy: galaxy target coordinate
        :param system: system target coordinate
        :return: tab index, or -1 if not found
        """
        cnt = self._tabwidget.count()
        if cnt < 3:
            return -1  # only overview and imperium tabs are present
        for index in range(2, cnt):
            tab_page = self._tabwidget.tabWidget(index)
            if tab_page is not None:
                try:
                    tab_type = tab_page.get_tab_type()
                    if tab_type == 'galaxy':
                        coords = tab_page.coords()
                        if (coords[0] == galaxy) and (coords[1] == system):
                            # we have found galaxy tab index where this place is already opened
                            return index
                except AttributeError:  # not all pages may have method get_tab_type()
                    pass
        return -1

    def test_setup_planets_panel(self):
        """
        Testing only - add 'fictive' planets to test planets panel without loading data
        :return: None
        """
        pl1 = XNPlanet('Arnon', XNCoords(1, 7, 6))
        pl1.pic_url = 'skins/default/planeten/small/s_normaltempplanet08.jpg'
        pl1.fields_busy = 90
        pl1.fields_total = 167
        pl1.is_current = True
        pl2 = XNPlanet('Safizon', XNCoords(1, 232, 7))
        pl2.pic_url = 'skins/default/planeten/small/s_dschjungelplanet05.jpg'
        pl2.fields_busy = 84
        pl2.fields_total = 207
        pl2.is_current = False
        test_planets = [pl1, pl2]
        self.setup_planets_panel(test_planets)

    def test_planet_tab(self):
        """
        Testing only - add 'fictive' planet tab to test UI without loading world
        :return:
        """
        # construct planet
        pl1 = XNPlanet('Arnon', coords=XNCoords(1, 7, 6), planet_id=12345)
        pl1.pic_url = 'skins/default/planeten/small/s_normaltempplanet08.jpg'
        pl1.fields_busy = 90
        pl1.fields_total = 167
        pl1.is_current = True
        pl1.res_current.met = 10000000
        pl1.res_current.cry = 50000
        pl1.res_current.deit = 250000000  # 250 mil
        pl1.res_per_hour.met = 60000
        pl1.res_per_hour.cry = 30000
        pl1.res_per_hour.deit = 15000
        pl1.res_max_silos.met = 6000000
        pl1.res_max_silos.cry = 3000000
        pl1.res_max_silos.deit = 1000000
        pl1.energy.energy_left = 10
        pl1.energy.energy_total = 1962
        pl1.energy.charge_percent = 92
        # planet building item
        bitem = XNPlanetBuildingItem()
        bitem.gid = 1
        bitem.name = 'Рудник металла'
        bitem.level = 29
        bitem.remove_link = ''
        bitem.build_link = '?set=buildings&cmd=insert&building={0}'.format(bitem.gid)
        bitem.seconds_total = 23746
        bitem.cost_met = 7670042
        bitem.cost_cry = 1917510
        bitem.is_building_item = True
        # second bitem
        bitem2 = XNPlanetBuildingItem()
        bitem2.gid = 2
        bitem2.name = 'Рудник кристалла'
        bitem2.level = 26
        bitem2.remove_link = ''
        bitem2.build_link = '?set=buildings&cmd=insert&building={0}'.format(bitem2.gid)
        bitem2.seconds_total = 13746
        bitem2.cost_met = 9735556
        bitem2.cost_cry = 4667778
        bitem2.is_building_item = True
        bitem2.is_downgrade = True
        bitem2.seconds_left = bitem2.seconds_total // 2
        bitem2.calc_end_time()
        # add bitems
        pl1.buildings_items = [bitem, bitem2]
        # add
        self.add_tab_for_planet(pl1)
Beispiel #15
0
class TrayIcon(QtCore.QObject):
    def __init__(self, widget: QtWidgets.QWidget, icon: QtGui.QIcon, **kwargs):
        super().__init__()
        self.menu_actions: Dict[str, MenuAction] = {}

        self.on_trigger: Optional[Callable] = kwargs.get('on_trigger')
        self.on_double_click: Optional[Callable] = kwargs.get(
            'on_double_click')
        self.on_middle_click: Optional[Callable] = kwargs.get(
            'on_middle_click')
        self.on_message_click: Optional[Callable] = kwargs.get(
            'on_message_click')

        self.context_menu = QtWidgets.QMenu()

        self.tray_available = QSystemTrayIcon.isSystemTrayAvailable()
        self.messages_available = QSystemTrayIcon.supportsMessages()

        self.tray_icon = QSystemTrayIcon(widget)
        self.tray_icon.setIcon(icon)
        self.tray_icon.setContextMenu(self.context_menu)

        self._connect_signals()

    def show(self):
        if not self.tray_available:
            return
        self._build_context_menu()
        self.tray_icon.show()

    def destroy(self):
        if not self.tray_available:
            return
        self.tray_icon.deleteLater()

    def add_menu_item(self, name: Optional[str], **kwargs):
        on_activate: Optional[Callable] = kwargs.get('on_activate')
        is_visible: Optional[Callable] = kwargs.get('is_visible')
        data: Optional[Any] = kwargs.get('data')
        group: Optional[str] = kwargs.get('group')

        if name is None:
            name = f"--{len(self.menu_actions):02d}"

        self.menu_actions[name] = MenuAction(on_activate, is_visible, data,
                                             group)

    def _message(self,
                 title: str,
                 message: str,
                 icon: Union[QSystemTrayIcon.MessageIcon,
                             QtGui.QIcon] = QSystemTrayIcon.Information,
                 delay: int = 10000):
        if not self.tray_available:
            return
        if self.messages_available:
            self.tray_icon.showMessage(title, message, icon, delay)
        else:
            print(f"{title}: {message}")

    def info(self, title: str, message: str, delay: int = 10000):
        self._message(title, message, QSystemTrayIcon.Information, delay)

    def warn(self, title: str, message: str, delay: int = 10000):
        self._message(title, message, QSystemTrayIcon.Warning, delay)

    def critical(self, title: str, message: str, delay: int = 10000):
        self._message(title, message, QSystemTrayIcon.Critical, delay)

    # noinspection PyUnresolvedReferences
    def _connect_signals(self):
        if not self.tray_available:
            return
        self.context_menu.aboutToShow.connect(self._build_context_menu)
        self.context_menu.triggered.connect(self._on_context_menu_action)
        self.tray_icon.messageClicked.connect(self._on_message_clicked)
        self.tray_icon.activated.connect(self._on_activated)

    def _on_activated(self, reason: QSystemTrayIcon.ActivationReason):
        if reason == QSystemTrayIcon.DoubleClick:
            if self.on_double_click is not None:
                self.on_double_click()
        elif reason == QSystemTrayIcon.MiddleClick:
            if self.on_middle_click is not None:
                self.on_middle_click()
        elif reason == QSystemTrayIcon.Trigger:
            if self.on_trigger is not None:
                self.on_trigger()

    def _on_message_clicked(self):
        if self.on_message_click is not None:
            self.on_message_click()

    def _build_context_menu(self):
        self.context_menu.clear()
        submenus = {}
        for name, menu_item in self.menu_actions.items():
            if name.startswith("--"):
                self.context_menu.addSeparator()
            else:
                if menu_item.is_visible is None or menu_item.is_visible():
                    group = menu_item.group
                    if group is not None:
                        submenu = submenus.get(group)
                        if submenu is None:
                            submenu = self.context_menu.addMenu(group)
                            submenus[group] = submenu
                        submenu.addAction(name)
                    else:
                        self.context_menu.addAction(name)

    def _on_context_menu_action(self, action: QtWidgets.QAction):
        if action is None:
            return
        selected_command = action.text()
        menu_action = self.menu_actions[selected_command]
        on_activate = menu_action.on_activate
        if on_activate is not None:
            if menu_action.data is not None:
                on_activate(data=menu_action.data)
                return
            on_activate()
Beispiel #16
0
class TriblerWindow(QMainWindow):
    resize_event = pyqtSignal()
    escape_pressed = pyqtSignal()
    tribler_crashed = pyqtSignal(str)
    received_search_completions = pyqtSignal(object)

    def on_exception(self, *exc_info):
        if self.exception_handler_called:
            # We only show one feedback dialog, even when there are two consecutive exceptions.
            return

        self.exception_handler_called = True

        exception_text = "".join(traceback.format_exception(*exc_info))
        logging.error(exception_text)
        self.tribler_crashed.emit(exception_text)

        self.delete_tray_icon()

        # Stop the download loop
        self.downloads_page.stop_loading_downloads()

        # Add info about whether we are stopping Tribler or not
        os.environ['TRIBLER_SHUTTING_DOWN'] = str(
            self.core_manager.shutting_down)

        if not self.core_manager.shutting_down:
            self.core_manager.stop(stop_app_on_shutdown=False)

        self.setHidden(True)

        if self.debug_window:
            self.debug_window.setHidden(True)

        dialog = FeedbackDialog(self, exception_text, self.tribler_version,
                                self.start_time)
        dialog.show()

    def __init__(self,
                 core_args=None,
                 core_env=None,
                 api_port=None,
                 api_key=None):
        QMainWindow.__init__(self)

        QCoreApplication.setOrganizationDomain("nl")
        QCoreApplication.setOrganizationName("TUDelft")
        QCoreApplication.setApplicationName("Tribler")

        self.gui_settings = QSettings()
        api_port = api_port or int(
            get_gui_setting(self.gui_settings, "api_port", DEFAULT_API_PORT))
        api_key = api_key or get_gui_setting(
            self.gui_settings, "api_key",
            hexlify(os.urandom(16)).encode('utf-8'))
        self.gui_settings.setValue("api_key", api_key)
        request_manager.port, request_manager.key = api_port, api_key

        self.navigation_stack = []
        self.tribler_started = False
        self.tribler_settings = None
        # TODO: move version_id to tribler_common and get core version in the core crash message
        self.tribler_version = version_id
        self.debug_window = None
        self.core_manager = CoreManager(api_port, api_key)
        self.pending_requests = {}
        self.pending_uri_requests = []
        self.download_uri = None
        self.dialog = None
        self.create_dialog = None
        self.chosen_dir = None
        self.new_version_dialog = None
        self.start_download_dialog_active = False
        self.selected_torrent_files = []
        self.vlc_available = True
        self.has_search_results = False
        self.last_search_query = None
        self.last_search_time = None
        self.start_time = time.time()
        self.exception_handler_called = False
        self.token_refresh_timer = None
        self.shutdown_timer = None
        self.add_torrent_url_dialog_active = False

        sys.excepthook = self.on_exception

        uic.loadUi(get_ui_file_path('mainwindow.ui'), self)
        TriblerRequestManager.window = self
        self.tribler_status_bar.hide()

        self.token_balance_widget.mouseReleaseEvent = self.on_token_balance_click

        def on_state_update(new_state):
            self.loading_text_label.setText(new_state)

        self.core_manager.core_state_update.connect(on_state_update)

        self.magnet_handler = MagnetHandler(self.window)
        QDesktopServices.setUrlHandler("magnet", self.magnet_handler,
                                       "on_open_magnet_link")

        self.debug_pane_shortcut = QShortcut(QKeySequence("Ctrl+d"), self)
        self.debug_pane_shortcut.activated.connect(
            self.clicked_menu_button_debug)
        self.import_torrent_shortcut = QShortcut(QKeySequence("Ctrl+o"), self)
        self.import_torrent_shortcut.activated.connect(
            self.on_add_torrent_browse_file)
        self.add_torrent_url_shortcut = QShortcut(QKeySequence("Ctrl+i"), self)
        self.add_torrent_url_shortcut.activated.connect(
            self.on_add_torrent_from_url)

        # Remove the focus rect on OS X
        for widget in self.findChildren(QLineEdit) + self.findChildren(
                QListWidget) + self.findChildren(QTreeWidget):
            widget.setAttribute(Qt.WA_MacShowFocusRect, 0)

        self.menu_buttons = [
            self.left_menu_button_search,
            self.left_menu_button_my_channel,
            self.left_menu_button_subscriptions,
            self.left_menu_button_video_player,
            self.left_menu_button_downloads,
            self.left_menu_button_discovered,
            self.left_menu_button_trust_graph,
        ]

        self.search_results_page.initialize_content_page(self.gui_settings)
        self.search_results_page.channel_torrents_filter_input.setHidden(True)

        self.settings_page.initialize_settings_page()
        self.downloads_page.initialize_downloads_page()
        self.loading_page.initialize_loading_page()
        self.discovering_page.initialize_discovering_page()

        self.discovered_page.initialize_content_page(self.gui_settings)
        self.discovered_page.initialize_root_model(
            DiscoveredChannelsModel(
                channel_info={"name": "Discovered channels"},
                endpoint_url="channels",
                hide_xxx=get_gui_setting(self.gui_settings,
                                         "family_filter",
                                         True,
                                         is_bool=True),
            ))
        self.core_manager.events_manager.discovered_channel.connect(
            self.discovered_page.model.on_new_entry_received)

        self.trust_page.initialize_trust_page()
        self.trust_graph_page.initialize_trust_graph()

        self.stackedWidget.setCurrentIndex(PAGE_LOADING)

        # Create the system tray icon
        if QSystemTrayIcon.isSystemTrayAvailable():
            self.tray_icon = QSystemTrayIcon()
            use_monochrome_icon = get_gui_setting(self.gui_settings,
                                                  "use_monochrome_icon",
                                                  False,
                                                  is_bool=True)
            self.update_tray_icon(use_monochrome_icon)

            # Create the tray icon menu
            menu = self.create_add_torrent_menu()
            show_downloads_action = QAction('Show downloads', self)
            show_downloads_action.triggered.connect(
                self.clicked_menu_button_downloads)
            token_balance_action = QAction('Show token balance', self)
            token_balance_action.triggered.connect(
                lambda: self.on_token_balance_click(None))
            quit_action = QAction('Quit Tribler', self)
            quit_action.triggered.connect(self.close_tribler)
            menu.addSeparator()
            menu.addAction(show_downloads_action)
            menu.addAction(token_balance_action)
            menu.addSeparator()
            menu.addAction(quit_action)
            self.tray_icon.setContextMenu(menu)
        else:
            self.tray_icon = None

        self.hide_left_menu_playlist()
        self.left_menu_button_debug.setHidden(True)
        self.top_menu_button.setHidden(True)
        self.left_menu.setHidden(True)
        self.token_balance_widget.setHidden(True)
        self.settings_button.setHidden(True)
        self.add_torrent_button.setHidden(True)
        self.top_search_bar.setHidden(True)

        # Set various icons
        self.top_menu_button.setIcon(QIcon(get_image_path('menu.png')))

        self.search_completion_model = QStringListModel()
        completer = QCompleter()
        completer.setModel(self.search_completion_model)
        completer.setCompletionMode(QCompleter.UnfilteredPopupCompletion)
        self.item_delegate = QStyledItemDelegate()
        completer.popup().setItemDelegate(self.item_delegate)
        completer.popup().setStyleSheet("""
        QListView {
            background-color: #404040;
        }

        QListView::item {
            color: #D0D0D0;
            padding-top: 5px;
            padding-bottom: 5px;
        }

        QListView::item:hover {
            background-color: #707070;
        }
        """)
        self.top_search_bar.setCompleter(completer)

        # Toggle debug if developer mode is enabled
        self.window().left_menu_button_debug.setHidden(not get_gui_setting(
            self.gui_settings, "debug", False, is_bool=True))

        # Start Tribler
        self.core_manager.start(core_args=core_args, core_env=core_env)

        self.core_manager.events_manager.torrent_finished.connect(
            self.on_torrent_finished)
        self.core_manager.events_manager.new_version_available.connect(
            self.on_new_version_available)
        self.core_manager.events_manager.tribler_started.connect(
            self.on_tribler_started)
        self.core_manager.events_manager.low_storage_signal.connect(
            self.on_low_storage)
        self.core_manager.events_manager.tribler_shutdown_signal.connect(
            self.on_tribler_shutdown_state_update)

        # Install signal handler for ctrl+c events
        def sigint_handler(*_):
            self.close_tribler()

        signal.signal(signal.SIGINT, sigint_handler)

        self.installEventFilter(self.video_player_page)

        # Resize the window according to the settings
        center = QApplication.desktop().availableGeometry(self).center()
        pos = self.gui_settings.value(
            "pos",
            QPoint(center.x() - self.width() * 0.5,
                   center.y() - self.height() * 0.5))
        size = self.gui_settings.value("size", self.size())

        self.move(pos)
        self.resize(size)

        self.show()

        self.add_to_channel_dialog = AddToChannelDialog(self.window())

    def update_tray_icon(self, use_monochrome_icon):
        if not QSystemTrayIcon.isSystemTrayAvailable() or not self.tray_icon:
            return

        if use_monochrome_icon:
            self.tray_icon.setIcon(
                QIcon(QPixmap(get_image_path('monochrome_tribler.png'))))
        else:
            self.tray_icon.setIcon(
                QIcon(QPixmap(get_image_path('tribler.png'))))
        self.tray_icon.show()

    def delete_tray_icon(self):
        if self.tray_icon:
            try:
                self.tray_icon.deleteLater()
            except RuntimeError:
                # The tray icon might have already been removed when unloading Qt.
                # This is due to the C code actually being asynchronous.
                logging.debug(
                    "Tray icon already removed, no further deletion necessary."
                )
            self.tray_icon = None

    def on_low_storage(self):
        """
        Dealing with low storage space available. First stop the downloads and the core manager and ask user to user to
        make free space.
        :return:
        """
        self.downloads_page.stop_loading_downloads()
        self.core_manager.shutting_down = True
        self.core_manager.stop(False)
        close_dialog = ConfirmationDialog(
            self.window(),
            "<b>CRITICAL ERROR</b>",
            "You are running low on disk space (<100MB). Please make sure to have "
            "sufficient free space available and restart Tribler again.",
            [("Close Tribler", BUTTON_TYPE_NORMAL)],
        )
        close_dialog.button_clicked.connect(lambda _: self.close_tribler())
        close_dialog.show()

    def on_torrent_finished(self, torrent_info):
        if "hidden" not in torrent_info or not torrent_info["hidden"]:
            self.tray_show_message(
                "Download finished",
                "Download of %s has finished." % torrent_info["name"])

    def show_loading_screen(self):
        self.top_menu_button.setHidden(True)
        self.left_menu.setHidden(True)
        self.token_balance_widget.setHidden(True)
        self.settings_button.setHidden(True)
        self.add_torrent_button.setHidden(True)
        self.top_search_bar.setHidden(True)
        self.stackedWidget.setCurrentIndex(PAGE_LOADING)

    def tray_set_tooltip(self, message):
        """
        Set a tooltip message for the tray icon, if possible.

        :param message: the message to display on hover
        """
        if self.tray_icon:
            try:
                self.tray_icon.setToolTip(message)
            except RuntimeError as e:
                logging.error("Failed to set tray tooltip: %s", str(e))

    def tray_show_message(self, title, message):
        """
        Show a message at the tray icon, if possible.

        :param title: the title of the message
        :param message: the message to display
        """
        if self.tray_icon:
            try:
                self.tray_icon.showMessage(title, message)
            except RuntimeError as e:
                logging.error("Failed to set tray message: %s", str(e))

    def on_tribler_started(self, version):
        if self.tribler_started:
            logging.warning("Received duplicate Tribler Core started event")
            return
        self.tribler_started = True
        self.tribler_version = version

        self.top_menu_button.setHidden(False)
        self.left_menu.setHidden(False)
        self.token_balance_widget.setHidden(False)
        self.settings_button.setHidden(False)
        self.add_torrent_button.setHidden(False)
        self.top_search_bar.setHidden(False)

        # fetch the settings, needed for the video player port
        self.fetch_settings()

        self.downloads_page.start_loading_downloads()

        self.setAcceptDrops(True)
        self.setWindowTitle("Tribler %s" % self.tribler_version)

        self.add_to_channel_dialog.load_channel(0)
        self.discovered_page.reset_view()

        # We have to load the video player (and initialize VLC stuff) after spawning a subprocess to prevent a crash
        # on Mac in frozen environments. Also see https://github.com/Tribler/tribler/issues/5420.
        self.video_player_page.initialize_player()

        if not self.gui_settings.value(
                "first_discover",
                False) and not self.core_manager.use_existing_core:
            self.core_manager.events_manager.discovered_channel.connect(
                self.stop_discovering)
            self.window().gui_settings.setValue("first_discover", True)
            self.discovering_page.is_discovering = True
            self.stackedWidget.setCurrentIndex(PAGE_DISCOVERING)
        else:
            self.clicked_menu_button_discovered()
            self.left_menu_button_discovered.setChecked(True)

    def stop_discovering(self):
        if not self.discovering_page.is_discovering:
            return
        self.core_manager.events_manager.discovered_channel.disconnect(
            self.stop_discovering)
        self.discovering_page.is_discovering = False
        if self.stackedWidget.currentIndex() == PAGE_DISCOVERING:
            self.clicked_menu_button_discovered()
            self.left_menu_button_discovered.setChecked(True)

    def initialize_personal_channels_page(self):
        autocommit_enabled = (get_gui_setting(
            self.gui_settings, "autocommit_enabled", True, is_bool=True)
                              if self.gui_settings else True)
        self.personal_channel_page.initialize_content_page(self.gui_settings,
                                                           edit_enabled=True)
        self.personal_channel_page.default_channel_model = (
            SimplifiedPersonalChannelsModel
            if autocommit_enabled else PersonalChannelsModel)
        self.personal_channel_page.initialize_root_model(
            self.personal_channel_page.default_channel_model(
                hide_xxx=False,
                channel_info={
                    "name": "My channels",
                    "state": "Personal"
                },
                endpoint_url="channels/mychannel/0",
                exclude_deleted=autocommit_enabled,
            ))

        def on_dirty_response(response):
            if response and "dirty" in response:
                self.personal_channel_page.update_labels(
                    dirty=response.get("dirty", False))

        TriblerNetworkRequest("channels/mychannel/0/commit",
                              on_dirty_response,
                              method='GET')

    def on_events_started(self, json_dict):
        self.setWindowTitle("Tribler %s" % json_dict["version"])

    def show_status_bar(self, message):
        self.tribler_status_bar_label.setText(message)
        self.tribler_status_bar.show()

    def hide_status_bar(self):
        self.tribler_status_bar.hide()

    def process_uri_request(self):
        """
        Process a URI request if we have one in the queue.
        """
        if len(self.pending_uri_requests) == 0:
            return

        uri = self.pending_uri_requests.pop()

        # TODO: create a proper confirmation dialog to show results of adding .mdblob files
        # the case for .mdblob files is handled without torrentinfo endpoint
        if uri.startswith('file') and (uri.endswith('.mdblob')
                                       or uri.endswith('.mdblob.lz4')):
            TriblerNetworkRequest("downloads",
                                  lambda _: None,
                                  method='PUT',
                                  data={"uri": uri})
            return

        if uri.startswith('file') or uri.startswith('magnet'):
            self.start_download_from_uri(uri)

    def perform_start_download_request(
        self,
        uri,
        anon_download,
        safe_seeding,
        destination,
        selected_files,
        total_files=0,
        add_to_channel=False,
        callback=None,
    ):
        # Check if destination directory is writable
        is_writable, error = is_dir_writable(destination)
        if not is_writable:
            gui_error_message = (
                "Insufficient write permissions to <i>%s</i> directory. Please add proper "
                "write permissions on the directory and add the torrent again. %s"
                % (destination, error))
            ConfirmationDialog.show_message(self.window(),
                                            "Download error <i>%s</i>" % uri,
                                            gui_error_message, "OK")
            return

        selected_files_list = []
        if len(selected_files) != total_files:  # Not all files included
            selected_files_list = [filename for filename in selected_files]

        anon_hops = int(self.tribler_settings['download_defaults']
                        ['number_hops']) if anon_download else 0
        safe_seeding = 1 if safe_seeding else 0
        post_data = {
            "uri": uri,
            "anon_hops": anon_hops,
            "safe_seeding": safe_seeding,
            "destination": destination,
            "selected_files": selected_files_list,
        }
        TriblerNetworkRequest("downloads",
                              callback if callback else self.on_download_added,
                              method='PUT',
                              data=post_data)

        # Save the download location to the GUI settings
        current_settings = get_gui_setting(self.gui_settings,
                                           "recent_download_locations", "")
        recent_locations = current_settings.split(
            ",") if len(current_settings) > 0 else []
        if isinstance(destination, str):
            destination = destination.encode('utf-8')
        encoded_destination = hexlify(destination)
        if encoded_destination in recent_locations:
            recent_locations.remove(encoded_destination)
        recent_locations.insert(0, encoded_destination)

        if len(recent_locations) > 5:
            recent_locations = recent_locations[:5]

        self.gui_settings.setValue("recent_download_locations",
                                   ','.join(recent_locations))

        if add_to_channel:

            def on_add_button_pressed(channel_id):
                post_data = {}
                if uri.startswith("file:"):
                    with open(uri[5:], "rb") as torrent_file:
                        post_data['torrent'] = b64encode(
                            torrent_file.read()).decode('utf8')
                elif uri.startswith("magnet:"):
                    post_data['uri'] = uri

                if post_data:
                    TriblerNetworkRequest(
                        f"channels/mychannel/{channel_id}/torrents",
                        lambda _: self.tray_show_message(
                            "Channel update",
                            "Torrent(s) added to your channel"),
                        method='PUT',
                        data=post_data,
                    )

            self.window().add_to_channel_dialog.show_dialog(
                on_add_button_pressed, confirm_button_text="Add torrent")

    def on_new_version_available(self, version):
        if version == str(self.gui_settings.value('last_reported_version')):
            return

        self.new_version_dialog = ConfirmationDialog(
            self,
            "New version available",
            "Version %s of Tribler is available.Do you want to visit the "
            "website to download the newest version?" % version,
            [('IGNORE', BUTTON_TYPE_NORMAL), ('LATER', BUTTON_TYPE_NORMAL),
             ('OK', BUTTON_TYPE_NORMAL)],
        )
        self.new_version_dialog.button_clicked.connect(
            lambda action: self.on_new_version_dialog_done(version, action))
        self.new_version_dialog.show()

    def on_new_version_dialog_done(self, version, action):
        if action == 0:  # ignore
            self.gui_settings.setValue("last_reported_version", version)
        elif action == 2:  # ok
            import webbrowser

            webbrowser.open("https://tribler.org")
        if self.new_version_dialog:
            self.new_version_dialog.close_dialog()
            self.new_version_dialog = None

    def on_search_text_change(self, text):
        # We do not want to bother the database on petty 1-character queries
        if len(text) < 2:
            return
        TriblerNetworkRequest("search/completions",
                              self.on_received_search_completions,
                              url_params={'q': sanitize_for_fts(text)})

    def on_received_search_completions(self, completions):
        if completions is None:
            return
        self.received_search_completions.emit(completions)
        self.search_completion_model.setStringList(completions["completions"])

    def fetch_settings(self):
        TriblerNetworkRequest("settings",
                              self.received_settings,
                              capture_errors=False)

    def received_settings(self, settings):
        if not settings:
            return
        # If we cannot receive the settings, stop Tribler with an option to send the crash report.
        if 'error' in settings:
            raise RuntimeError(
                TriblerRequestManager.get_message_from_error(settings))

        # If there is any pending dialog (likely download dialog or error dialog of setting not available),
        # close the dialog
        if self.dialog:
            self.dialog.close_dialog()
            self.dialog = None

        self.tribler_settings = settings['settings']

        # Set the video server port
        self.video_player_page.video_player_port = self.core_manager.api_port
        self.video_player_page.video_player_api_key = self.core_manager.api_key.decode(
            'utf-8')

        self.downloads_all_button.click()

        # process pending file requests (i.e. someone clicked a torrent file when Tribler was closed)
        # We do this after receiving the settings so we have the default download location.
        self.process_uri_request()

        # Set token balance refresh timer and load the token balance
        self.token_refresh_timer = QTimer()
        self.token_refresh_timer.timeout.connect(self.load_token_balance)
        self.token_refresh_timer.start(60000)

        self.load_token_balance()

    def on_top_search_button_click(self):
        current_ts = time.time()
        query = self.top_search_bar.text()

        if (self.last_search_query and self.last_search_time
                and self.last_search_query == self.top_search_bar.text()
                and current_ts - self.last_search_time < 1):
            logging.info(
                "Same search query already sent within 500ms so dropping this one"
            )
            return

        self.left_menu_button_search.setChecked(True)
        self.has_search_results = True
        self.search_results_page.initialize_root_model(
            SearchResultsModel(
                channel_info={
                    "name":
                    "Search results for %s" %
                    query if len(query) < 50 else "%s..." % query[:50]
                },
                endpoint_url="search",
                hide_xxx=get_gui_setting(self.gui_settings,
                                         "family_filter",
                                         True,
                                         is_bool=True),
                text_filter=to_fts_query(query),
            ))

        # Trigger remote search
        self.search_results_page.preview_clicked()

        self.clicked_menu_button_search()
        self.last_search_query = query
        self.last_search_time = current_ts

    def on_settings_button_click(self):
        self.deselect_all_menu_buttons()
        self.stackedWidget.setCurrentIndex(PAGE_SETTINGS)
        self.settings_page.load_settings()
        self.navigation_stack = []
        self.hide_left_menu_playlist()

    def on_token_balance_click(self, _):
        self.raise_window()
        self.deselect_all_menu_buttons()
        self.stackedWidget.setCurrentIndex(PAGE_TRUST)
        self.load_token_balance()
        self.trust_page.load_blocks()
        self.navigation_stack = []
        self.hide_left_menu_playlist()

    def load_token_balance(self):
        TriblerNetworkRequest("trustchain/statistics",
                              self.received_trustchain_statistics,
                              capture_errors=False)

    def received_trustchain_statistics(self, statistics):
        if not statistics or "statistics" not in statistics:
            return

        self.trust_page.received_trustchain_statistics(statistics)

        statistics = statistics["statistics"]
        balance = statistics.get("total_up", 0) - statistics.get(
            "total_down", 0)
        self.set_token_balance(balance)

        # If trust page is currently visible, then load the graph as well
        if self.stackedWidget.currentIndex() == PAGE_TRUST:
            self.trust_page.load_blocks()

    def set_token_balance(self, balance):
        if abs(balance) > 1024**4:  # Balance is over a TB
            balance /= 1024.0**4
            self.token_balance_label.setText("%.1f TB" % balance)
        elif abs(balance) > 1024**3:  # Balance is over a GB
            balance /= 1024.0**3
            self.token_balance_label.setText("%.1f GB" % balance)
        else:
            balance /= 1024.0**2
            self.token_balance_label.setText("%d MB" % balance)

    def raise_window(self):
        self.setWindowState(self.windowState() & ~Qt.WindowMinimized
                            | Qt.WindowActive)
        self.raise_()
        self.activateWindow()

    def create_add_torrent_menu(self):
        """
        Create a menu to add new torrents. Shows when users click on the tray icon or the big plus button.
        """
        menu = TriblerActionMenu(self)

        browse_files_action = QAction('Import torrent from file', self)
        browse_directory_action = QAction('Import torrent(s) from directory',
                                          self)
        add_url_action = QAction('Import torrent from magnet/URL', self)
        add_mdblob_action = QAction('Import Tribler metadata from file', self)
        create_torrent_action = QAction('Create torrent from file(s)', self)

        browse_files_action.triggered.connect(self.on_add_torrent_browse_file)
        browse_directory_action.triggered.connect(
            self.on_add_torrent_browse_dir)
        add_url_action.triggered.connect(self.on_add_torrent_from_url)
        add_mdblob_action.triggered.connect(self.on_add_mdblob_browse_file)
        create_torrent_action.triggered.connect(self.on_create_torrent)

        menu.addAction(browse_files_action)
        menu.addAction(browse_directory_action)
        menu.addAction(add_url_action)
        menu.addAction(add_mdblob_action)
        menu.addSeparator()
        menu.addAction(create_torrent_action)

        return menu

    def on_create_torrent(self):
        if self.create_dialog:
            self.create_dialog.close_dialog()

        self.create_dialog = CreateTorrentDialog(self)
        self.create_dialog.create_torrent_notification.connect(
            self.on_create_torrent_updates)
        self.create_dialog.show()

    def on_create_torrent_updates(self, update_dict):
        self.tray_show_message("Torrent updates", update_dict['msg'])

    def on_add_torrent_button_click(self, _pos):
        plus_btn_pos = self.add_torrent_button.pos()
        plus_btn_geometry = self.add_torrent_button.geometry()
        plus_btn_pos.setX(plus_btn_pos.x() - CONTEXT_MENU_WIDTH)
        plus_btn_pos.setY(plus_btn_pos.y() + plus_btn_geometry.height())
        self.create_add_torrent_menu().exec_(self.mapToParent(plus_btn_pos))

    def on_add_torrent_browse_file(self):
        filenames = QFileDialog.getOpenFileNames(
            self, "Please select the .torrent file", QDir.homePath(),
            "Torrent files (*.torrent)")
        if len(filenames[0]) > 0:
            for filename in filenames[0]:
                self.pending_uri_requests.append(u"file:%s" % filename)
            self.process_uri_request()

    def on_add_mdblob_browse_file(self):
        filenames = QFileDialog.getOpenFileNames(
            self, "Please select the .mdblob file", QDir.homePath(),
            "Tribler metadata files (*.mdblob.lz4)")
        if len(filenames[0]) > 0:
            for filename in filenames[0]:
                self.pending_uri_requests.append(u"file:%s" % filename)
            self.process_uri_request()

    def start_download_from_uri(self, uri):
        uri = uri.decode('utf-8') if isinstance(uri, bytes) else uri
        self.download_uri = uri

        if get_gui_setting(self.gui_settings,
                           "ask_download_settings",
                           True,
                           is_bool=True):
            # FIXME: instead of using this workaround, make sure the settings are _available_ by this moment
            # If tribler settings is not available, fetch the settings and inform the user to try again.
            if not self.tribler_settings:
                self.fetch_settings()
                self.dialog = ConfirmationDialog.show_error(
                    self,
                    "Download Error",
                    "Tribler settings is not available\
                                                                   yet. Fetching it now. Please try again later.",
                )
                # By re-adding the download uri to the pending list, the request is re-processed
                # when the settings is received
                self.pending_uri_requests.append(uri)
                return
            # Clear any previous dialog if exists
            if self.dialog:
                self.dialog.close_dialog()
                self.dialog = None

            self.dialog = StartDownloadDialog(self, self.download_uri)
            self.dialog.button_clicked.connect(self.on_start_download_action)
            self.dialog.show()
            self.start_download_dialog_active = True
        else:
            # FIXME: instead of using this workaround, make sure the settings are _available_ by this moment
            # In the unlikely scenario that tribler settings are not available yet, try to fetch settings again and
            # add the download uri back to self.pending_uri_requests to process again.
            if not self.tribler_settings:
                self.fetch_settings()
                if self.download_uri not in self.pending_uri_requests:
                    self.pending_uri_requests.append(self.download_uri)
                return

            self.window().perform_start_download_request(
                self.download_uri,
                self.window().tribler_settings['download_defaults']
                ['anonymity_enabled'],
                self.window().tribler_settings['download_defaults']
                ['safeseeding_enabled'],
                self.tribler_settings['download_defaults']['saveas'],
                [],
            )
            self.process_uri_request()

    def on_start_download_action(self, action):
        if action == 1:
            if self.dialog and self.dialog.dialog_widget:
                self.window().perform_start_download_request(
                    self.download_uri,
                    self.dialog.dialog_widget.anon_download_checkbox.isChecked(
                    ),
                    self.dialog.dialog_widget.safe_seed_checkbox.isChecked(),
                    self.dialog.dialog_widget.destination_input.currentText(),
                    self.dialog.get_selected_files(),
                    self.dialog.dialog_widget.files_list_view.
                    topLevelItemCount(),
                    add_to_channel=self.dialog.dialog_widget.
                    add_to_channel_checkbox.isChecked(),
                )
            else:
                ConfirmationDialog.show_error(
                    self, "Tribler UI Error",
                    "Something went wrong. Please try again.")
                logging.exception(
                    "Error while trying to download. Either dialog or dialog.dialog_widget is None"
                )

        if self.dialog:
            self.dialog.close_dialog()
            self.dialog = None
            self.start_download_dialog_active = False

        if action == 0:  # We do this after removing the dialog since process_uri_request is blocking
            self.process_uri_request()

    def on_add_torrent_browse_dir(self):
        chosen_dir = QFileDialog.getExistingDirectory(
            self, "Please select the directory containing the .torrent files",
            QDir.homePath(), QFileDialog.ShowDirsOnly)
        self.chosen_dir = chosen_dir
        if len(chosen_dir) != 0:
            self.selected_torrent_files = [
                torrent_file
                for torrent_file in glob.glob(chosen_dir + "/*.torrent")
            ]
            self.dialog = ConfirmationDialog(
                self,
                "Add torrents from directory",
                "Add %s torrent files from the following directory "
                "to your Tribler channel:\n\n%s" %
                (len(self.selected_torrent_files), chosen_dir),
                [('ADD', BUTTON_TYPE_NORMAL), ('CANCEL', BUTTON_TYPE_CONFIRM)],
                checkbox_text="Add torrents to My Channel",
            )
            self.dialog.button_clicked.connect(
                self.on_confirm_add_directory_dialog)
            self.dialog.show()

    def on_confirm_add_directory_dialog(self, action):
        if action == 0:
            if self.dialog.checkbox.isChecked():
                # TODO: add recursive directory scanning
                def on_add_button_pressed(channel_id):
                    TriblerNetworkRequest(
                        f"collections/mychannel/{channel_id}/torrents",
                        lambda _: self.tray_show_message(
                            "Channels update",
                            f"{self.chosen_dir} added to your channel"),
                        method='PUT',
                        data={"torrents_dir": self.chosen_dir},
                    )

                self.window().add_to_channel_dialog.show_dialog(
                    on_add_button_pressed,
                    confirm_button_text="Add torrent(s)")

            for torrent_file in self.selected_torrent_files:
                self.perform_start_download_request(
                    u"file:%s" % torrent_file,
                    self.window().tribler_settings['download_defaults']
                    ['anonymity_enabled'],
                    self.window().tribler_settings['download_defaults']
                    ['safeseeding_enabled'],
                    self.tribler_settings['download_defaults']['saveas'],
                    [],
                )

        if self.dialog:
            self.dialog.close_dialog()
            self.dialog = None

    def on_add_torrent_from_url(self):
        # Make sure that the window is visible (this action might be triggered from the tray icon)
        self.raise_window()

        if self.video_player_page.isVisible():
            # If we're adding a torrent from the video player page, go to the home page.
            # This is necessary since VLC takes the screen and the popup becomes invisible.
            self.clicked_menu_button_downloads()

        if not self.add_torrent_url_dialog_active:
            self.dialog = ConfirmationDialog(
                self,
                "Add torrent from URL/magnet link",
                "Please enter the URL/magnet link in the field below:",
                [('ADD', BUTTON_TYPE_NORMAL), ('CANCEL', BUTTON_TYPE_CONFIRM)],
                show_input=True,
            )
            self.dialog.dialog_widget.dialog_input.setPlaceholderText(
                'URL/magnet link')
            self.dialog.dialog_widget.dialog_input.setFocus()
            self.dialog.button_clicked.connect(
                self.on_torrent_from_url_dialog_done)
            self.dialog.show()
            self.add_torrent_url_dialog_active = True

    def on_torrent_from_url_dialog_done(self, action):
        self.add_torrent_url_dialog_active = False
        if self.dialog and self.dialog.dialog_widget:
            uri = self.dialog.dialog_widget.dialog_input.text().strip()

            # If the URI is a 40-bytes hex-encoded infohash, convert it to a valid magnet link
            if len(uri) == 40:
                valid_ih_hex = True
                try:
                    int(uri, 16)
                except ValueError:
                    valid_ih_hex = False

                if valid_ih_hex:
                    uri = "magnet:?xt=urn:btih:" + uri

            # Remove first dialog
            self.dialog.close_dialog()
            self.dialog = None

            if action == 0:
                self.start_download_from_uri(uri)

    def on_download_added(self, result):
        if not result:
            return
        if len(self.pending_uri_requests
               ) == 0:  # Otherwise, we first process the remaining requests.
            self.window().left_menu_button_downloads.click()
        else:
            self.process_uri_request()

    def on_top_menu_button_click(self):
        if self.left_menu.isHidden():
            self.left_menu.show()
        else:
            self.left_menu.hide()

    def deselect_all_menu_buttons(self, except_select=None):
        for button in self.menu_buttons:
            if button == except_select:
                button.setEnabled(False)
                continue
            button.setEnabled(True)

            if button == self.left_menu_button_search and not self.has_search_results:
                button.setEnabled(False)

            button.setChecked(False)

    def clicked_menu_button_search(self):
        self.deselect_all_menu_buttons()
        self.left_menu_button_search.setChecked(True)
        if self.stackedWidget.currentIndex() == PAGE_SEARCH_RESULTS:
            self.search_results_page.go_back_to_level(0)
        self.stackedWidget.setCurrentIndex(PAGE_SEARCH_RESULTS)
        self.search_results_page.content_table.setFocus()
        self.navigation_stack = []
        self.hide_left_menu_playlist()

    def clicked_menu_button_discovered(self):
        self.deselect_all_menu_buttons()
        self.left_menu_button_discovered.setChecked(True)
        if self.stackedWidget.currentIndex() == PAGE_DISCOVERED:
            self.discovered_page.go_back_to_level(0)
            self.discovered_page.reset_view()
        self.stackedWidget.setCurrentIndex(PAGE_DISCOVERED)
        self.discovered_page.content_table.setFocus()
        self.navigation_stack = []
        self.hide_left_menu_playlist()

    def clicked_menu_button_my_channel(self):
        self.deselect_all_menu_buttons()
        self.left_menu_button_my_channel.setChecked(True)
        if not self.personal_channel_page.channels_stack:
            self.initialize_personal_channels_page()
            self.personal_channel_page.reset_view()
        if self.stackedWidget.currentIndex() == PAGE_EDIT_CHANNEL:
            self.personal_channel_page.go_back_to_level(0)
            self.personal_channel_page.reset_view()
        self.stackedWidget.setCurrentIndex(PAGE_EDIT_CHANNEL)
        self.personal_channel_page.content_table.setFocus()
        self.navigation_stack = []
        self.hide_left_menu_playlist()

    def clicked_menu_button_video_player(self):
        self.deselect_all_menu_buttons(self.left_menu_button_video_player)
        self.stackedWidget.setCurrentIndex(PAGE_VIDEO_PLAYER)
        self.navigation_stack = []
        self.show_left_menu_playlist()

    def clicked_menu_button_trust_graph(self):
        self.deselect_all_menu_buttons(self.left_menu_button_trust_graph)
        self.stackedWidget.setCurrentIndex(PAGE_TRUST_GRAPH_PAGE)
        self.navigation_stack = []
        self.hide_left_menu_playlist()

    def clicked_menu_button_downloads(self):
        self.deselect_all_menu_buttons(self.left_menu_button_downloads)
        self.raise_window()
        self.left_menu_button_downloads.setChecked(True)
        self.stackedWidget.setCurrentIndex(PAGE_DOWNLOADS)
        self.navigation_stack = []
        self.hide_left_menu_playlist()

    def clicked_menu_button_debug(self):
        if not self.debug_window:
            self.debug_window = DebugWindow(self.tribler_settings,
                                            self.tribler_version)
        self.debug_window.show()

    def clicked_menu_button_subscriptions(self):
        self.deselect_all_menu_buttons()
        self.left_menu_button_subscriptions.setChecked(True)
        if not self.subscribed_channels_page.channels_stack:
            self.subscribed_channels_page.initialize_content_page(
                self.gui_settings)
            self.subscribed_channels_page.initialize_root_model(
                DiscoveredChannelsModel(
                    channel_info={"name": "Subscribed channels"},
                    endpoint_url="channels",
                    subscribed_only=True))
            self.subscribed_channels_page.reset_view()

        if self.stackedWidget.currentIndex() == PAGE_SUBSCRIBED_CHANNELS:
            self.subscribed_channels_page.go_back_to_level(0)
            self.subscribed_channels_page.reset_view()
        self.stackedWidget.setCurrentIndex(PAGE_SUBSCRIBED_CHANNELS)
        self.subscribed_channels_page.content_table.setFocus()
        self.navigation_stack = []
        self.hide_left_menu_playlist()

    def hide_left_menu_playlist(self):
        self.left_menu_seperator.setHidden(True)
        self.left_menu_playlist_label.setHidden(True)
        self.left_menu_playlist.setHidden(True)

    def show_left_menu_playlist(self):
        self.left_menu_seperator.setHidden(False)
        self.left_menu_playlist_label.setHidden(False)
        self.left_menu_playlist.setHidden(False)

    def on_page_back_clicked(self):
        try:
            prev_page = self.navigation_stack.pop()
            self.stackedWidget.setCurrentIndex(prev_page)
        except IndexError:
            logging.exception("Unknown page found in stack")

    def resizeEvent(self, _):
        # This thing here is necessary to send the resize event to dialogs, etc.
        self.resize_event.emit()

    def exit_full_screen(self):
        self.top_bar.show()
        self.left_menu.show()
        self.video_player_page.is_full_screen = False
        self.showNormal()

    def close_tribler(self):
        if not self.core_manager.shutting_down:

            def show_force_shutdown():
                self.window().force_shutdown_btn.show()

            self.delete_tray_icon()
            self.show_loading_screen()
            self.hide_status_bar()
            self.loading_text_label.setText("Shutting down...")
            if self.debug_window:
                self.debug_window.setHidden(True)

            self.shutdown_timer = QTimer()
            self.shutdown_timer.timeout.connect(show_force_shutdown)
            self.shutdown_timer.start(SHUTDOWN_WAITING_PERIOD)

            self.gui_settings.setValue("pos", self.pos())
            self.gui_settings.setValue("size", self.size())

            if self.core_manager.use_existing_core:
                # Don't close the core that we are using
                QApplication.quit()

            self.core_manager.stop()
            self.core_manager.shutting_down = True
            self.downloads_page.stop_loading_downloads()
            self.video_player_page.reset_player()
            request_manager.clear()

            # Stop the token balance timer
            if self.token_refresh_timer:
                self.token_refresh_timer.stop()

    def closeEvent(self, close_event):
        self.close_tribler()
        close_event.ignore()

    def keyReleaseEvent(self, event):
        if event.key() == Qt.Key_Escape:
            self.escape_pressed.emit()
            if self.isFullScreen():
                self.exit_full_screen()

    def dragEnterEvent(self, e):
        file_urls = [_qurl_to_path(url) for url in e.mimeData().urls()
                     ] if e.mimeData().hasUrls() else []

        if any(os.path.isfile(filename) for filename in file_urls):
            e.accept()
        else:
            e.ignore()

    def dropEvent(self, e):
        file_urls = ([(_qurl_to_path(url), url.toString())
                      for url in e.mimeData().urls()]
                     if e.mimeData().hasUrls() else [])

        for filename, fileurl in file_urls:
            if os.path.isfile(filename):
                self.start_download_from_uri(fileurl)

        e.accept()

    def clicked_force_shutdown(self):
        process_checker = ProcessChecker()
        if process_checker.already_running:
            core_pid = process_checker.get_pid_from_lock_file()
            os.kill(int(core_pid), 9)
        # Stop the Qt application
        QApplication.quit()

    def clicked_skip_conversion(self):
        self.dialog = ConfirmationDialog(
            self,
            "Abort the conversion of old channels",
            "The upgrade procedure is now converting torrents in channels "
            "collected by the previous installation of Tribler.\n\n"
            "Are you sure you want to abort the conversion process?\n",
            [('ABORT', BUTTON_TYPE_CONFIRM), ('CONTINUE', BUTTON_TYPE_NORMAL)],
        )
        self.dialog.button_clicked.connect(self.on_skip_conversion_dialog)
        self.dialog.show()

    def on_skip_conversion_dialog(self, action):
        if action == 0:
            TriblerNetworkRequest("upgrader",
                                  lambda _: None,
                                  data={"skip_db_upgrade": True},
                                  method='POST')

        if self.dialog:
            self.dialog.close_dialog()
            self.dialog = None

    def on_tribler_shutdown_state_update(self, state):
        self.loading_text_label.setText(state)