Esempio n. 1
0
class DownloadsPage(AddBreadcrumbOnShowMixin, QWidget):
    """
    This class is responsible for managing all items on the downloads page.
    The downloads page shows all downloads and specific details about a download.
    """

    received_downloads = pyqtSignal(object)

    def __init__(self):
        QWidget.__init__(self)
        self.export_dir = None
        self.filter = DOWNLOADS_FILTER_ALL
        self.download_widgets = {}  # key: infohash, value: QTreeWidgetItem
        self.downloads = None
        self.downloads_timer = QTimer()
        self.downloads_timeout_timer = QTimer()
        self.downloads_last_update = 0
        self.selected_items = []
        self.dialog = None
        self.loading_message_widget = None
        self.total_download = 0
        self.total_upload = 0

        self.rest_request = None

    def showEvent(self, QShowEvent):
        """
        When the downloads tab is clicked, we want to update the downloads list immediately.
        """
        super().showEvent(QShowEvent)
        self.stop_loading_downloads()
        self.schedule_downloads_timer(True)

    def initialize_downloads_page(self):
        self.window().downloads_tab.initialize()
        connect(self.window().downloads_tab.clicked_tab_button,
                self.on_downloads_tab_button_clicked)

        connect(self.window().start_download_button.clicked,
                self.on_start_download_clicked)
        connect(self.window().stop_download_button.clicked,
                self.on_stop_download_clicked)
        connect(self.window().remove_download_button.clicked,
                self.on_remove_download_clicked)

        connect(self.window().downloads_list.itemSelectionChanged,
                self.on_download_item_clicked)

        connect(self.window().downloads_list.customContextMenuRequested,
                self.on_right_click_item)

        self.window().download_details_widget.initialize_details_widget()
        self.window().download_details_widget.hide()

        connect(self.window().downloads_filter_input.textChanged,
                self.on_filter_text_changed)

        self.window().downloads_list.header().setSortIndicator(
            12, Qt.AscendingOrder)
        self.window().downloads_list.header().resizeSection(12, 146)

        self.downloads_timeout_timer.setSingleShot(True)
        self.downloads_timer.setSingleShot(True)
        connect(self.downloads_timer.timeout, self.load_downloads)
        connect(self.downloads_timeout_timer.timeout,
                self.on_downloads_request_timeout)

    def on_filter_text_changed(self, text):
        self.window().downloads_list.clearSelection()
        self.window().download_details_widget.hide()
        self.update_download_visibility()

    def start_loading_downloads(self):
        self.window().downloads_list.setSelectionMode(
            QAbstractItemView.NoSelection)
        self.loading_message_widget = QTreeWidgetItem()
        self.window().downloads_list.addTopLevelItem(
            self.loading_message_widget)
        self.window().downloads_list.setItemWidget(
            self.loading_message_widget, 2,
            LoadingListItem(self.window().downloads_list))
        self.schedule_downloads_timer(now=True)

    def schedule_downloads_timer(self, now=False):
        self.downloads_timer.start(0 if now else 1000)
        self.downloads_timeout_timer.start(16000)

    def on_downloads_request_timeout(self):
        if self.rest_request:
            self.rest_request.cancel_request()
        self.schedule_downloads_timer()

    def stop_loading_downloads(self):
        self.downloads_timer.stop()
        self.downloads_timeout_timer.stop()

    def load_downloads(self):
        url = "downloads?get_pieces=1"
        if self.window().download_details_widget.currentIndex() == 3:
            url += "&get_peers=1"
        elif self.window().download_details_widget.currentIndex() == 1:
            url += "&get_files=1"

        isactive = not self.isHidden()

        if isactive or (time.time() - self.downloads_last_update > 30):
            # Update if the downloads page is visible or if we haven't updated for longer than 30 seconds
            self.downloads_last_update = time.time()
            priority = QNetworkRequest.LowPriority if not isactive else QNetworkRequest.HighPriority
            if self.rest_request:
                self.rest_request.cancel_request()
            self.rest_request = TriblerNetworkRequest(
                url, self.on_received_downloads, priority=priority)

    def on_received_downloads(self, downloads):
        if not downloads or "downloads" not in downloads:
            return  # This might happen when closing Tribler
        loading_widget_index = self.window(
        ).downloads_list.indexOfTopLevelItem(self.loading_message_widget)
        if loading_widget_index > -1:
            self.window().downloads_list.takeTopLevelItem(loading_widget_index)
            self.window().downloads_list.setSelectionMode(
                QAbstractItemView.ExtendedSelection)

        self.downloads = downloads

        self.total_download = 0
        self.total_upload = 0

        download_infohashes = set()

        items = []
        for download in downloads["downloads"]:
            # Update download progress information for torrents in the Channels GUI.
            # We skip updating progress information for ChannelTorrents because otherwise it interferes
            # with channel processing progress updates
            if not download["channel_download"]:
                self.window(
                ).core_manager.events_manager.node_info_updated.emit({
                    "infohash":
                    download["infohash"],
                    "progress":
                    download["progress"]
                })

            if download["infohash"] in self.download_widgets:
                item = self.download_widgets[download["infohash"]]
            else:
                item = DownloadWidgetItem()
                self.download_widgets[download["infohash"]] = item
                items.append(item)

            item.update_with_download(download)

            self.total_download += download["speed_down"]
            self.total_upload += download["speed_up"]

            download_infohashes.add(download["infohash"])

            if (self.window().download_details_widget.current_download
                    is not None and self.window().download_details_widget.
                    current_download["infohash"] == download["infohash"]):
                self.window(
                ).download_details_widget.current_download = download
                self.window().download_details_widget.update_pages()

        self.window().downloads_list.addTopLevelItems(items)
        for item in items:
            self.window().downloads_list.setItemWidget(item, 2,
                                                       item.bar_container)

        # Check whether there are download that should be removed
        for infohash, item in list(self.download_widgets.items()):
            if infohash not in download_infohashes:
                index = self.window().downloads_list.indexOfTopLevelItem(item)
                self.window().downloads_list.takeTopLevelItem(index)
                del self.download_widgets[infohash]

        self.window().tray_set_tooltip(
            f"Down: {format_speed(self.total_download)}, Up: {format_speed(self.total_upload)}"
        )
        self.update_download_visibility()
        self.schedule_downloads_timer()

        # Update the top download management button if we have a row selected
        if len(self.window().downloads_list.selectedItems()) > 0:
            self.on_download_item_clicked()

        self.received_downloads.emit(downloads)

    def update_download_visibility(self):
        for i in range(self.window().downloads_list.topLevelItemCount()):
            item = self.window().downloads_list.topLevelItem(i)
            if not isinstance(item, DownloadWidgetItem):
                continue

            filter_match = self.window().downloads_filter_input.text().lower(
            ) in item.download_info["name"].lower()
            is_channel = item.download_info["channel_download"]
            if self.filter == DOWNLOADS_FILTER_CHANNELS:
                item.setHidden(not is_channel or not filter_match)
            else:
                item.setHidden(not item.get_raw_download_status()
                               in DOWNLOADS_FILTER_DEFINITION[self.filter]
                               or not filter_match or is_channel)

    def on_downloads_tab_button_clicked(self, button_name):
        self.filter = button_name2filter[button_name]

        self.window().downloads_list.clearSelection()
        self.window().download_details_widget.hide()
        self.update_download_visibility()

    @staticmethod
    def start_download_enabled(download_widgets):
        return any([
            download_widget.get_raw_download_status() == DLSTATUS_STOPPED
            for download_widget in download_widgets
        ])

    @staticmethod
    def stop_download_enabled(download_widgets):
        return any([
            download_widget.get_raw_download_status()
            not in [DLSTATUS_STOPPED, DLSTATUS_STOPPED_ON_ERROR]
            for download_widget in download_widgets
        ])

    @staticmethod
    def force_recheck_download_enabled(download_widgets):
        return any([
            download_widget.get_raw_download_status() not in [
                DLSTATUS_METADATA, DLSTATUS_HASHCHECKING,
                DLSTATUS_WAITING4HASHCHECK
            ] for download_widget in download_widgets
        ])

    def on_download_item_clicked(self):
        selected_count = len(self.window().downloads_list.selectedItems())
        if selected_count == 0:
            self.window().remove_download_button.setEnabled(False)
            self.window().start_download_button.setEnabled(False)
            self.window().stop_download_button.setEnabled(False)
            self.window().download_details_widget.hide()
        elif selected_count == 1:
            self.selected_items = self.window().downloads_list.selectedItems()
            self.window().remove_download_button.setEnabled(True)
            self.window().start_download_button.setEnabled(
                DownloadsPage.start_download_enabled(self.selected_items))
            self.window().stop_download_button.setEnabled(
                DownloadsPage.stop_download_enabled(self.selected_items))

            self.window().download_details_widget.update_with_download(
                self.selected_items[0].download_info)
            self.window().download_details_widget.show()
        else:
            self.selected_items = self.window().downloads_list.selectedItems()
            self.window().remove_download_button.setEnabled(True)
            self.window().start_download_button.setEnabled(
                DownloadsPage.start_download_enabled(self.selected_items))
            self.window().stop_download_button.setEnabled(
                DownloadsPage.stop_download_enabled(self.selected_items))
            self.window().download_details_widget.hide()

    def on_start_download_clicked(self, checked):
        for selected_item in self.selected_items:
            infohash = selected_item.download_info["infohash"]
            TriblerNetworkRequest(f"downloads/{infohash}",
                                  self.on_download_resumed,
                                  method='PATCH',
                                  data={"state": "resume"})

    def on_download_resumed(self, json_result):
        if json_result and 'modified' in json_result:
            for selected_item in self.selected_items:
                if selected_item.download_info["infohash"] == json_result[
                        "infohash"]:
                    selected_item.download_info[
                        'status'] = "DLSTATUS_DOWNLOADING"
                    selected_item.update_item()
                    self.on_download_item_clicked()

    def on_stop_download_clicked(self, checked):
        for selected_item in self.selected_items:
            infohash = selected_item.download_info["infohash"]
            TriblerNetworkRequest(f"downloads/{infohash}",
                                  self.on_download_stopped,
                                  method='PATCH',
                                  data={"state": "stop"})

    def on_download_stopped(self, json_result):
        if json_result and "modified" in json_result:
            for selected_item in self.selected_items:
                if selected_item.download_info["infohash"] == json_result[
                        "infohash"]:
                    selected_item.download_info['status'] = "DLSTATUS_STOPPED"
                    selected_item.update_item()
                    self.on_download_item_clicked()

    def on_remove_download_clicked(self, checked):
        self.dialog = ConfirmationDialog(
            self,
            tr("Remove download"),
            tr("Are you sure you want to remove this download?"),
            [
                (tr("remove download"), BUTTON_TYPE_NORMAL),
                (tr("remove download + data"), BUTTON_TYPE_NORMAL),
                (tr("cancel"), BUTTON_TYPE_CONFIRM),
            ],
        )
        connect(self.dialog.button_clicked, self.on_remove_download_dialog)
        self.dialog.show()

    def on_remove_download_dialog(self, action):
        if action != 2:
            for selected_item in self.selected_items:
                infohash = selected_item.download_info["infohash"]

                TriblerNetworkRequest(
                    f"downloads/{infohash}",
                    self.on_download_removed,
                    method='DELETE',
                    data={"remove_data": bool(action)},
                )
        if self.dialog:
            self.dialog.close_dialog()
            self.dialog = None

    def on_download_removed(self, json_result):
        if json_result and "removed" in json_result:
            self.load_downloads()
            self.window().download_details_widget.hide()

    def on_force_recheck_download(self, checked):
        for selected_item in self.selected_items:
            infohash = selected_item.download_info["infohash"]
            TriblerNetworkRequest(f"downloads/{infohash}",
                                  self.on_forced_recheck,
                                  method='PATCH',
                                  data={"state": "recheck"})

    def on_forced_recheck(self, result):
        if result and "modified" in result:
            for selected_item in self.selected_items:
                if selected_item.download_info["infohash"] == result[
                        "infohash"]:
                    selected_item.download_info[
                        'status'] = "DLSTATUS_HASHCHECKING"
                    selected_item.update_item()
                    self.on_download_item_clicked()

    def on_change_anonymity(self, result):
        pass

    def change_anonymity(self, hops):
        for selected_item in self.selected_items:
            infohash = selected_item.download_info["infohash"]
            TriblerNetworkRequest(f"downloads/{infohash}",
                                  self.on_change_anonymity,
                                  method='PATCH',
                                  data={"anon_hops": hops})

    def on_explore_files(self, checked):
        # ACHTUNG! To whomever might stumble upon here intending to debug the case
        # when this does not work on Linux: know, my friend, that for some mysterious reason
        # (probably related to Snap disk access rights peculiarities), this DOES NOT work
        # when you run Tribler from PyCharm. However, it works perfectly fine when you
        # run Tribler directly from system console, etc. So, don't spend your time on debugging this,
        # like I did.
        for selected_item in self.selected_items:
            path = os.path.normpath(selected_item.download_info["destination"])
            QDesktopServices.openUrl(QUrl.fromLocalFile(path))

    def on_move_files(self, checked):
        if len(self.selected_items) != 1:
            return

        dest_dir = QFileDialog.getExistingDirectory(
            self,
            tr("Please select the destination directory"),
            self.selected_items[0].download_info["destination"],
            QFileDialog.ShowDirsOnly,
        )
        if not dest_dir:
            return

        _infohash = self.selected_items[0].download_info["infohash"]
        _name = self.selected_items[0].download_info["name"]

        data = {"state": "move_storage", "dest_dir": dest_dir}

        TriblerNetworkRequest(
            f"downloads/{_infohash}",
            lambda res: self.on_files_moved(res, _name, dest_dir),
            data=data,
            method='PATCH',
        )

    def on_files_moved(self, response, name, dest_dir):
        if "modified" in response and response["modified"]:
            self.window().tray_show_message(name, f"Moved to {dest_dir}")

    def on_export_download(self, checked):
        self.export_dir = QFileDialog.getExistingDirectory(
            self, tr("Please select the destination directory"), "",
            QFileDialog.ShowDirsOnly)

        selected_item = self.selected_items[:1]
        if len(self.export_dir) > 0 and selected_item:
            # Show confirmation dialog where we specify the name of the file
            torrent_name = selected_item[0].download_info['name']
            self.dialog = ConfirmationDialog(
                self,
                tr("Export torrent file"),
                tr("Please enter the name of the torrent file:"),
                [(tr("SAVE"), BUTTON_TYPE_NORMAL),
                 (tr("CANCEL"), BUTTON_TYPE_CONFIRM)],
                show_input=True,
            )
            self.dialog.dialog_widget.dialog_input.setPlaceholderText(
                tr("Torrent file name"))
            self.dialog.dialog_widget.dialog_input.setText(
                f"{torrent_name}.torrent")
            self.dialog.dialog_widget.dialog_input.setFocus()
            connect(self.dialog.button_clicked,
                    self.on_export_download_dialog_done)
            self.dialog.show()

    def on_export_download_dialog_done(self, action):
        selected_item = self.selected_items[:1]
        if action == 0 and selected_item:
            filename = self.dialog.dialog_widget.dialog_input.text()
            TriblerFileDownloadRequest(
                f"downloads/{selected_item[0].download_info['infohash']}/torrent",
                lambda data: self.on_export_download_request_done(
                    filename, data),
            )

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

    def on_export_download_request_done(self, filename, data):
        dest_path = os.path.join(self.export_dir, filename)
        try:
            torrent_file = open(dest_path, "wb")
            torrent_file.write(data)
            torrent_file.close()
        except OSError as exc:
            ConfirmationDialog.show_error(
                self.window(),
                tr("Error when exporting file"),
                tr("An error occurred when exporting the torrent file: %s") %
                str(exc),
            )
        else:
            self.window().tray_show_message(
                tr("Torrent file exported"),
                tr("Torrent file exported to %s") % str(dest_path))

    def on_add_to_channel(self, checked):
        def on_add_button_pressed(channel_id):
            for selected_item in self.selected_items:
                infohash = selected_item.download_info["infohash"]
                name = selected_item.download_info["name"]
                TriblerNetworkRequest(
                    f"channels/mychannel/{channel_id}/torrents",
                    lambda _: self.window().tray_show_message(
                        tr("Channel update"),
                        tr("Torrent(s) added to your channel")),
                    method='PUT',
                    data={"uri": compose_magnetlink(infohash, name=name)},
                )

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

    def on_right_click_item(self, pos):
        item_clicked = self.window().downloads_list.itemAt(pos)
        if not item_clicked or self.selected_items is None:
            return

        if item_clicked not in self.selected_items:
            self.selected_items.append(item_clicked)

        menu = TriblerActionMenu(self)

        start_action = QAction(tr("Start"), self)
        stop_action = QAction(tr("Stop"), self)
        remove_download_action = QAction(tr("Remove download"), self)
        add_to_channel_action = QAction(tr("Add to my channel"), self)
        force_recheck_action = QAction(tr("Force recheck"), self)
        export_download_action = QAction(tr("Export .torrent file"), self)
        explore_files_action = QAction(tr("Explore files"), self)
        move_files_action = QAction(tr("Move file storage"), self)

        no_anon_action = QAction(tr("No anonymity"), self)
        one_hop_anon_action = QAction(tr("One hop"), self)
        two_hop_anon_action = QAction(tr("Two hops"), self)
        three_hop_anon_action = QAction(tr("Three hops"), self)

        connect(start_action.triggered, self.on_start_download_clicked)
        start_action.setEnabled(
            DownloadsPage.start_download_enabled(self.selected_items))
        connect(stop_action.triggered, self.on_stop_download_clicked)
        stop_action.setEnabled(
            DownloadsPage.stop_download_enabled(self.selected_items))
        connect(add_to_channel_action.triggered, self.on_add_to_channel)
        connect(remove_download_action.triggered,
                self.on_remove_download_clicked)
        connect(force_recheck_action.triggered, self.on_force_recheck_download)
        force_recheck_action.setEnabled(
            DownloadsPage.force_recheck_download_enabled(self.selected_items))
        connect(export_download_action.triggered, self.on_export_download)
        connect(explore_files_action.triggered, self.on_explore_files)
        connect(move_files_action.triggered, self.on_move_files)

        connect(no_anon_action.triggered, lambda _: self.change_anonymity(0))
        connect(one_hop_anon_action.triggered,
                lambda _: self.change_anonymity(1))
        connect(two_hop_anon_action.triggered,
                lambda _: self.change_anonymity(2))
        connect(three_hop_anon_action.triggered,
                lambda _: self.change_anonymity(3))

        menu.addAction(start_action)
        menu.addAction(stop_action)

        menu.addSeparator()
        menu.addAction(add_to_channel_action)
        menu.addSeparator()
        menu.addAction(remove_download_action)
        menu.addSeparator()
        menu.addAction(force_recheck_action)
        menu.addSeparator()

        exclude_states = [
            DLSTATUS_METADATA,
            DLSTATUS_CIRCUITS,
            DLSTATUS_EXIT_NODES,
            DLSTATUS_HASHCHECKING,
            DLSTATUS_WAITING4HASHCHECK,
        ]
        if len(self.selected_items) == 1 and self.selected_items[
                0].get_raw_download_status() not in exclude_states:
            menu.addAction(export_download_action)
        menu.addAction(explore_files_action)
        if len(self.selected_items) == 1:
            menu.addAction(move_files_action)
        menu.addSeparator()

        menu_anon_level = menu.addMenu(tr("Change Anonymity "))
        menu_anon_level.addAction(no_anon_action)
        menu_anon_level.addAction(one_hop_anon_action)
        menu_anon_level.addAction(two_hop_anon_action)
        menu_anon_level.addAction(three_hop_anon_action)

        menu.exec_(self.window().downloads_list.mapToGlobal(pos))
Esempio n. 2
0
class ChannelContentsWidget(AddBreadcrumbOnShowMixin, widget_form,
                            widget_class):
    def __init__(self, parent=None):
        widget_class.__init__(self, parent=parent)

        # ACHTUNG! This is a dumb workaround for a bug(?) in PyQT bindings in Python 3.7
        # When more than a single instance of a class is created, every next setupUi
        # triggers connectSlotsByName error. There are some reports that it is specific to
        # 3.7 and there is a fix in the 10.08.2019 PyQt bindings snapshot.
        try:
            self.setupUi(self)
        except SystemError:
            pass

        # ! ACHTUNG !
        # There is a bug in PyQT bindings that prevents uic.loadUiType from correctly
        # detecting paths to external resources used in .ui files. Therefore,
        # for each external resource (e.g. image/icon), we must reload it manually here.
        self.channel_options_button.setIcon(
            QIcon(get_image_path('ellipsis.png')))

        self.default_channel_model = ChannelContentModel

        self.initialized = False
        self.chosen_dir = None
        self.dialog = None
        self.controller = None
        self.commit_timer = None
        self.autocommit_enabled = None
        self.channel_options_menu = None

        self.channels_stack = []
        self.categories = ()

        self_ref = self

        self.hide_xxx = None

        # This context manager is used to freeze the state of controls in the models stack to
        # prevent signals from triggering for inactive models.
        class freeze_controls_class:
            objects_to_freeze = [
                self_ref.category_selector,
                self_ref.content_table.horizontalHeader(),
                self_ref.channel_torrents_filter_input,
            ]

            def __enter__(self):
                for obj in self.objects_to_freeze:
                    obj.blockSignals(True)

            def __exit__(self, *args):
                for obj in self.objects_to_freeze:
                    obj.blockSignals(False)

        self.freeze_controls = freeze_controls_class
        self.channel_description_container.setHidden(True)

        self.explanation_tooltip_button.setHidden(True)

    def hide_all_labels(self):
        self.edit_channel_contents_top_bar.setHidden(True)
        self.subscription_widget.setHidden(True)
        self.channel_num_torrents_label.setHidden(True)
        self.channel_state_label.setHidden(True)

    @property
    def personal_channel_model(self):
        return SimplifiedPersonalChannelsModel if self.autocommit_enabled else PersonalChannelsModel

    @property
    def model(self):
        return self.channels_stack[-1] if self.channels_stack else None

    def on_channel_committed(self, response):
        if not response or not response.get("success", False):
            return

        if not self.autocommit_enabled:
            self.commit_control_bar.setHidden(True)

        if not self.model:
            return

        info = self.model.channel_info
        if info.get("state") == "Personal" and info.get("dirty"):
            self.model.reset()
            self.update_labels()

    def commit_channels(self, checked=False):  # pylint: disable=W0613
        TriblerNetworkRequest("channels/mychannel/0/commit",
                              self.on_channel_committed,
                              method='POST')

    def initialize_content_page(
        self,
        autocommit_enabled=False,
        hide_xxx=None,
        controller_class=ContentTableViewController,
        categories=CATEGORY_SELECTOR_FOR_SEARCH_ITEMS,
    ):
        if self.initialized:
            return

        self.hide_xxx = hide_xxx
        self.initialized = True
        self.categories = categories
        self.category_selector.addItems(self.categories)
        connect(self.category_selector.currentIndexChanged,
                self.on_category_selector_changed)
        self.channel_back_button.setIcon(QIcon(
            get_image_path('page_back.png')))
        connect(self.channel_back_button.clicked, self.go_back)
        connect(self.channel_name_label.linkActivated,
                self.on_breadcrumb_clicked)
        self.commit_control_bar.setHidden(True)

        if LINUX:
            # On Linux, the default font sometimes does not contain the emoji characters.
            self.category_selector.setStyleSheet(
                "font-family: Noto Color Emoji")

        self.controller = controller_class(
            self.content_table,
            filter_input=self.channel_torrents_filter_input)

        # Hide channel description on scroll
        connect(self.controller.table_view.verticalScrollBar().valueChanged,
                self._on_table_scroll)

        self.autocommit_enabled = autocommit_enabled
        if self.autocommit_enabled:
            self._enable_autocommit_timer()

        # New channel button
        connect(self.new_channel_button.clicked, self.create_new_channel)
        connect(self.content_table.channel_clicked, self.on_channel_clicked)
        connect(self.edit_channel_commit_button.clicked, self.commit_channels)

        self.subscription_widget.initialize(self)

        self.channel_options_menu = self.create_channel_options_menu()
        self.channel_options_button.setMenu(self.channel_options_menu)
        connect(self.channel_description_container.became_hidden,
                self.run_brain_dead_refresh)
        connect(self.channel_description_container.description_changed,
                self._description_changed)

    def _description_changed(self):
        # Initialize commit timer on channel description change
        if self.autocommit_enabled:
            self.commit_timer.stop()
            self.commit_timer.start(CHANNEL_COMMIT_DELAY)
        self.model.channel_info["dirty"] = True
        self.update_labels()

    def run_brain_dead_refresh(self):
        if self.model:
            self.controller.brain_dead_refresh()

    def _on_table_scroll(self, event):  # pylint: disable=unused-argument
        # Hide the description widget when the channel is scrolled down
        if not self.model.data_items:
            return

        scrollbar = self.controller.table_view.verticalScrollBar()
        container = self.channel_description_container

        is_time_to_hide = scrollbar.minimum(
        ) < scrollbar.value() - 10 and scrollbar.maximum() > 100
        is_time_to_show = scrollbar.minimum() == scrollbar.value()

        if is_time_to_hide and not container.isHidden():
            container.setHidden(True)
        elif is_time_to_show and container.isHidden(
        ) and container.initialized:
            container.setHidden(False)

    def _enable_autocommit_timer(self):
        self.commit_timer = QTimer()
        self.commit_timer.setSingleShot(True)
        connect(self.commit_timer.timeout, self.commit_channels)

        # Commit the channel just in case there are uncommitted changes left since the last time (e.g. Tribler crashed)
        # The timer thing here is a workaround for race condition with the core startup
        self.controller.table_view.setColumnHidden(3, True)
        self.commit_timer.stop()
        self.commit_timer.start(10000)

    def on_category_selector_changed(self, ind):
        category = self.categories[ind] if ind else None
        content_category = ContentCategories.get(category)
        category_code = content_category.code if content_category else category
        if self.model.category_filter != category_code:
            self.model.category_filter = category_code
            self.model.reset()

    def empty_channels_stack(self):
        if self.channels_stack:
            self.disconnect_current_model()
            self.channels_stack = []

    def push_channels_stack(self, model):
        if self.model:
            self.model.saved_header_state = self.controller.table_view.horizontalHeader(
            ).saveState()
            self.model.saved_scroll_state = self.controller.table_view.verticalScrollBar(
            ).value()
            self.disconnect_current_model()
        self.channels_stack.append(model)
        self.connect_current_model()

        with self.freeze_controls():
            self.category_selector.setCurrentIndex(0)
            self.content_table.horizontalHeader().setSortIndicator(
                -1, Qt.AscendingOrder)
            self.channel_torrents_filter_input.setText("")

    def on_model_info_changed(self, changed_entries):
        self.window().channels_menu_list.reload_if_necessary(changed_entries)
        dirty = False
        structure_changed = False
        for entry in changed_entries:
            dirty = dirty or entry.get('status', None) in DIRTY_STATUSES
            structure_changed = (
                structure_changed or entry.get("state", None) == "Deleted" or
                (entry.get("type", None) in [CHANNEL_TORRENT, COLLECTION_NODE]
                 and entry["status"] in DIRTY_STATUSES))

        if structure_changed:
            self.window().add_to_channel_dialog.clear_channels_tree()

        if self.autocommit_enabled and dirty:
            self.commit_timer.stop()
            self.commit_timer.start(CHANNEL_COMMIT_DELAY)

        self.model.channel_info["dirty"] = dirty
        self.update_labels()

    def initialize_root_model_from_channel_info(self, channel_info):
        if channel_info.get("state") == CHANNEL_STATE.PERSONAL.value:
            self.default_channel_model = self.personal_channel_model
        else:
            self.default_channel_model = ChannelContentModel
        model = self.default_channel_model(hide_xxx=self.hide_xxx,
                                           channel_info=channel_info)
        self.initialize_root_model(model)

    def initialize_root_model(self, root_model):
        self.empty_channels_stack()
        self.push_channels_stack(root_model)
        self.controller.set_model(self.model)
        # Hide the edit controls by default, to prevent the user clicking the buttons prematurely
        self.hide_all_labels()

    def reset_view(self, text_filter=None, category_filter=None):
        if not self.model:
            return
        self.model.text_filter = text_filter or ''
        self.model.category_filter = category_filter

        with self.freeze_controls():
            self._set_filter_controls_from_model()
            self.controller.table_view.horizontalHeader().setSortIndicator(
                -1, Qt.DescendingOrder)
        self.model.sort_by = (
            self.model.columns[self.model.default_sort_column].dict_key
            if self.model.default_sort_column >= 0 else None)
        self.model.sort_desc = True
        self.model.reset()

    def disconnect_current_model(self):
        disconnect(self.window().core_manager.events_manager.node_info_updated,
                   self.model.update_node_info)
        disconnect(self.model.info_changed, self.on_model_info_changed)
        self.controller.unset_model()  # Disconnect the selectionChanged signal

    def connect_current_model(self):
        connect(self.model.info_changed, self.on_model_info_changed)
        connect(self.window().core_manager.events_manager.node_info_updated,
                self.model.update_node_info)

    @property
    def current_level(self):
        return len(self.channels_stack) - 1

    def go_back(self, checked=False):  # pylint: disable=W0613
        self.go_back_to_level(self.current_level - 1)

    def on_breadcrumb_clicked(self, tgt_level):
        if int(tgt_level) != self.current_level:
            self.go_back_to_level(tgt_level)
        elif isinstance(self.model,
                        SearchResultsModel) and self.current_level == 0:
            # In case of remote search, when only the search results are on the stack,
            # we must keep the txt_filter and category_filter (which contains the search term)
            # before resetting the view
            self.reset_view(text_filter=self.model.text_filter,
                            category_filter=self.model.category_filter)
        else:
            # Reset the view if the user clicks on the last part of the breadcrumb
            self.reset_view()

    def _set_filter_controls_from_model(self):
        # This should typically be called under freeze_controls context manager
        content_category = ContentCategories.get(self.model.category_filter)
        filter_display_name = content_category.long_name if content_category else self.model.category_filter
        self.category_selector.setCurrentIndex(
            self.categories.index(filter_display_name)
            if filter_display_name in self.categories else 0)
        self.channel_torrents_filter_input.setText(self.model.text_filter
                                                   or '')

    def go_back_to_level(self, level):
        switched_level = False
        level = int(level)
        disconnected_current_model = False
        while 0 <= level < self.current_level:
            switched_level = True
            if not disconnected_current_model:
                disconnected_current_model = True
                self.disconnect_current_model()
            self.channels_stack.pop().deleteLater()

        if switched_level:
            self.channel_description_container.initialized = False
            # We block signals to prevent triggering redundant model reloading
            with self.freeze_controls():
                self._set_filter_controls_from_model()
                # Set filter category selector to correct index corresponding to loaded model
                self.controller.set_model(self.model)

            self.connect_current_model()
            self.update_labels()

    def on_channel_clicked(self, channel_dict):
        self.initialize_with_channel(channel_dict)

    def create_new_channel(self, checked):  # pylint: disable=W0613
        NewChannelDialog(self, self.model.create_new_channel)

    def initialize_with_channel(self, channel_info):
        # Hide the edit controls by default, to prevent the user clicking the buttons prematurely
        self.hide_all_labels()
        # Turn off sorting by default to speed up SQL queries
        if channel_info.get("state") == CHANNEL_STATE.PREVIEW.value:
            self.push_channels_stack(
                ChannelPreviewModel(channel_info=channel_info))
        else:
            self.push_channels_stack(
                self.default_channel_model(channel_info=channel_info))
        self.controller.set_model(self.model)
        self.update_navigation_breadcrumbs()
        self.controller.table_view.deselect_all_rows()
        self.controller.table_view.resizeEvent(None)

        self.content_table.setFocus()

    def update_navigation_breadcrumbs(self):
        # Assemble the channels navigation breadcrumb by utilising RichText links feature
        self.channel_name_label.setTextFormat(Qt.RichText)
        # We build the breadcrumb text backwards, by performing lookahead on each step.
        # While building the breadcrumb label in RichText we also assemble an undecorated variant of the same text
        # to estimate if we need to elide the breadcrumb. We cannot use RichText contents directly with
        # .elidedText method because QT will elide the tags as well.
        breadcrumb_text = ''
        breadcrumb_text_undecorated = ''
        path_parts = [(m, model.channel_info["name"])
                      for m, model in enumerate(self.channels_stack)]
        slash_separator = '<font color=#aaa>  /  </font>'
        for m, channel_name in reversed(path_parts):
            breadcrumb_text_undecorated = " / " + channel_name + breadcrumb_text_undecorated
            breadcrumb_text_elided = self.channel_name_label.fontMetrics(
            ).elidedText(breadcrumb_text_undecorated, 0,
                         self.channel_name_label.width())
            must_elide = breadcrumb_text_undecorated != breadcrumb_text_elided
            if must_elide:
                channel_name = "..."
            breadcrumb_text = (
                slash_separator +
                f'<a style="text-decoration:none;color:#eee;" href="{m}">{channel_name}</a>'
                + breadcrumb_text)
            if must_elide:
                break
        # Remove the leftmost slash:
        if len(breadcrumb_text) >= len(slash_separator):
            breadcrumb_text = breadcrumb_text[len(slash_separator):]

        self.channel_name_label.setText(breadcrumb_text)
        self.channel_name_label.setTextInteractionFlags(
            Qt.TextBrowserInteraction)

        self.channel_back_button.setHidden(self.current_level == 0)

        # Disabling focus on the label is necessary to remove the ugly dotted rectangle around the most recently
        # clicked part of the path.
        # ACHTUNG! Setting focus policy in the .ui file does not work for some reason!
        # Also, something changes the focus policy during the runtime, so we have to re-set it every time here.
        self.channel_name_label.setFocusPolicy(Qt.NoFocus)

    def update_labels(self):

        folder = self.model.channel_info.get("type", None) == COLLECTION_NODE
        personal = self.model.channel_info.get(
            "state", None) == CHANNEL_STATE.PERSONAL.value
        root = self.current_level == 0
        legacy = self.model.channel_info.get(
            "state", None) == CHANNEL_STATE.LEGACY.value
        discovered = isinstance(self.model, DiscoveredChannelsModel)
        personal_model = isinstance(self.model, PersonalChannelsModel)
        is_a_channel = self.model.channel_info.get("type",
                                                   None) == CHANNEL_TORRENT
        description_flag = self.model.channel_info.get("description_flag")
        thumbnail_flag = self.model.channel_info.get("thumbnail_flag")
        dirty = self.model.channel_info.get("dirty")

        self.update_navigation_breadcrumbs()

        info = self.model.channel_info
        container = self.channel_description_container
        if is_a_channel and (description_flag or thumbnail_flag
                             or personal_model):
            container.initialize_with_channel(info["public_key"],
                                              info["id"],
                                              edit=personal and personal_model)
        else:
            container.initialized = False
            container.setHidden(True)

        self.category_selector.setHidden(root
                                         and (discovered or personal_model))
        # initialize the channel page

        self.edit_channel_contents_top_bar.setHidden(not personal)
        self.new_channel_button.setText(
            tr("NEW CHANNEL"
               ) if not is_a_channel and not folder else tr("NEW FOLDER"))
        self.channel_options_button.setHidden(not personal_model
                                              or not personal
                                              or (root and not is_a_channel))
        self.new_channel_button.setHidden(not personal_model or not personal)

        self.channel_state_label.setText(
            self.model.channel_info.get("state",
                                        "This text should not ever be shown"))

        self.subscription_widget.setHidden(not is_a_channel or personal
                                           or folder or legacy)
        if not self.subscription_widget.isHidden():
            self.subscription_widget.update_subscribe_button(
                self.model.channel_info)

        self.channel_state_label.setHidden((root and not is_a_channel)
                                           or personal)

        self.commit_control_bar.setHidden(self.autocommit_enabled or not dirty
                                          or not personal)

        if "total" in self.model.channel_info:
            self.channel_num_torrents_label.setHidden(False)
            if "torrents" in self.model.channel_info:
                self.channel_num_torrents_label.setText(
                    tr("%(total)i/%(torrents)i items") %
                    self.model.channel_info)
            else:
                self.channel_num_torrents_label.setText(
                    tr("%(total)i items") % self.model.channel_info)
        else:
            self.channel_num_torrents_label.setHidden(True)

    # ==============================
    # Channel menu related methods.
    # ==============================

    def create_channel_options_menu(self):
        browse_files_action = QAction(tr("Add .torrent file"), self)
        browse_dir_action = QAction(tr("Add torrent(s) directory"), self)
        add_url_action = QAction(tr("Add URL/magnet links"), self)

        connect(browse_files_action.triggered, self.on_add_torrent_browse_file)
        connect(browse_dir_action.triggered, self.on_add_torrents_browse_dir)
        connect(add_url_action.triggered, self.on_add_torrent_from_url)

        channel_options_menu = TriblerActionMenu(self)
        channel_options_menu.addAction(browse_files_action)
        channel_options_menu.addAction(browse_dir_action)
        channel_options_menu.addAction(add_url_action)
        return channel_options_menu

    # Torrent addition-related methods
    def on_add_torrents_browse_dir(self, checked):  # pylint: disable=W0613
        chosen_dir = QFileDialog.getExistingDirectory(
            self,
            tr("Please select the directory containing the .torrent files"),
            QDir.homePath(),
            QFileDialog.ShowDirsOnly,
        )
        if not chosen_dir:
            return

        self.chosen_dir = chosen_dir
        self.dialog = ConfirmationDialog(
            self,
            tr("Add torrents from directory"),
            tr("Add all torrent files from the following directory to your Tribler channel: \n\n %s"
               ) % chosen_dir,
            [('ADD', BUTTON_TYPE_NORMAL), ('CANCEL', BUTTON_TYPE_CONFIRM)],
            checkbox_text=tr("Include subdirectories (recursive mode)"),
        )
        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:
            self.add_dir_to_channel(self.chosen_dir,
                                    recursive=self.dialog.checkbox.isChecked())

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

    def on_add_torrent_browse_file(self, checked):  # pylint: disable=W0613
        filenames = QFileDialog.getOpenFileNames(
            self,
            tr("Please select the .torrent file"),
            filter=(tr("Torrent files %s") % '(*.torrent)'))
        if not filenames[0]:
            return

        for filename in filenames[0]:
            self.add_torrent_to_channel(filename)

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

    def on_torrent_from_url_dialog_done(self, action):
        if action == 0:
            self.add_torrent_url_to_channel(
                self.dialog.dialog_widget.dialog_input.text())
        self.dialog.close_dialog()
        self.dialog = None

    def _on_torrent_to_channel_added(self, result):
        if not result:
            return
        if result.get('added'):
            # ACHTUNG: this is a dumb hack to adapt torrents PUT endpoint output to the info_changed signal.
            # If thousands of torrents were added, we don't want to post them all in a single
            # REST response. Instead, we always provide the total number of new torrents.
            # If we add a single torrent though, the endpoint will return it as a dict.
            # However, on_model_info_changed always expects a list of changed entries.
            # So, we make up the list.
            results_list = result['added']
            if isinstance(results_list, dict):
                results_list = [results_list]
            elif isinstance(results_list, int):
                results_list = [{'status': NEW}]
            self.model.info_changed.emit(results_list)
            self.model.reset()

    def _add_torrent_request(self, data):
        TriblerNetworkRequest(
            f'collections/mychannel/{self.model.channel_info["id"]}/torrents',
            self._on_torrent_to_channel_added,
            method='PUT',
            data=data,
        )

    def add_torrent_to_channel(self, filename):
        with open(filename, "rb") as torrent_file:
            torrent_content = b64encode(torrent_file.read()).decode('utf-8')
        self._add_torrent_request({"torrent": torrent_content})

    def add_dir_to_channel(self, dirname, recursive=False):
        self._add_torrent_request({
            "torrents_dir": dirname,
            "recursive": int(recursive)
        })

    def add_torrent_url_to_channel(self, url):
        self._add_torrent_request({"uri": url})
Esempio n. 3
0
class CreateTorrentPage(AddBreadcrumbOnShowMixin, QWidget):
    """
    The CreateTorrentPage is the page where users can create torrent files so they can be added to their channel.
    """
    def __init__(self):
        QWidget.__init__(self)

        self.channel_identifier = None
        self.dialog = None
        self.selected_item_index = -1
        self.initialized = False

    def initialize(self):
        self.window().create_torrent_name_field.setText('')
        self.window().create_torrent_description_field.setText('')
        self.window().create_torrent_files_list.clear()
        self.window().seed_after_adding_checkbox.setChecked(True)
        self.window().edit_channel_create_torrent_progress_label.hide()

        if not self.initialized:
            self.window().manage_channel_create_torrent_back.setIcon(
                QIcon(get_image_path('page_back.png')))

            connect(
                self.window().create_torrent_files_list.
                customContextMenuRequested, self.on_right_click_file_item)
            connect(self.window().manage_channel_create_torrent_back.clicked,
                    self.on_create_torrent_manage_back_clicked)
            connect(self.window().create_torrent_choose_files_button.clicked,
                    self.on_choose_files_clicked)
            connect(self.window().create_torrent_choose_dir_button.clicked,
                    self.on_choose_dir_clicked)
            connect(self.window().edit_channel_create_torrent_button.clicked,
                    self.on_create_clicked)

            self.initialized = True

    def on_create_torrent_manage_back_clicked(self, checked):
        self.window().edit_channel_details_stacked_widget.setCurrentIndex(
            PAGE_EDIT_CHANNEL_TORRENTS)

    def on_choose_files_clicked(self, checked):
        filenames, _ = QFileDialog.getOpenFileNames(self.window(),
                                                    "Please select the files",
                                                    QDir.homePath())

        for filename in filenames:
            self.window().create_torrent_files_list.addItem(filename)

    def on_choose_dir_clicked(self, checked):
        chosen_dir = QFileDialog.getExistingDirectory(
            self.window(), "Please select the directory containing the files",
            "", QFileDialog.ShowDirsOnly)

        if len(chosen_dir) == 0:
            return

        files = []
        for path, _, dir_files in os.walk(chosen_dir):
            for filename in dir_files:
                files.append(os.path.join(path, filename))

        self.window().create_torrent_files_list.clear()
        for filename in files:
            self.window().create_torrent_files_list.addItem(filename)

    def on_create_clicked(self, checked):
        if self.window().create_torrent_files_list.count() == 0:
            self.dialog = ConfirmationDialog(
                self, "Notice",
                "You should add at least one file to your torrent.",
                [('CLOSE', BUTTON_TYPE_NORMAL)])
            connect(self.dialog.button_clicked, self.on_dialog_ok_clicked)
            self.dialog.show()
            return

        self.window().edit_channel_create_torrent_button.setEnabled(False)

        files_list = []
        for ind in range(self.window().create_torrent_files_list.count()):
            file_str = self.window().create_torrent_files_list.item(ind).text()
            files_list.append(file_str)

        name = self.window().create_torrent_name_field.text()
        description = self.window(
        ).create_torrent_description_field.toPlainText()
        post_data = {
            "name": name,
            "description": description,
            "files": files_list
        }
        url = "createtorrent?download=1" if self.window(
        ).seed_after_adding_checkbox.isChecked() else "createtorrent"
        TriblerNetworkRequest(url,
                              self.on_torrent_created,
                              data=post_data,
                              method='POST')
        # Show creating torrent text
        self.window().edit_channel_create_torrent_progress_label.show()

    def on_dialog_ok_clicked(self, _):
        self.dialog.close_dialog()
        self.dialog = None

    def on_torrent_created(self, result):
        if not result:
            return
        self.window().edit_channel_create_torrent_button.setEnabled(True)
        if 'torrent' in result:
            self.add_torrent_to_channel(result['torrent'])

    def add_torrent_to_channel(self, torrent):
        TriblerNetworkRequest("mychannel/torrents",
                              self.on_torrent_to_channel_added,
                              data={"torrent": torrent},
                              method='PUT')

    def on_torrent_to_channel_added(self, result):
        if not result:
            return
        self.window().edit_channel_create_torrent_progress_label.hide()
        if 'added' in result:
            self.window().edit_channel_details_stacked_widget.setCurrentIndex(
                PAGE_EDIT_CHANNEL_TORRENTS)
            self.window().personal_channel_page.load_my_torrents()

    def on_remove_entry(self):
        self.window().create_torrent_files_list.takeItem(
            self.selected_item_index)

    def on_right_click_file_item(self, pos):
        item_clicked = self.window().create_torrent_files_list.itemAt(pos)
        if not item_clicked:
            return

        self.selected_item_index = self.window().create_torrent_files_list.row(
            item_clicked)

        menu = TriblerActionMenu(self)

        remove_action = QAction('Remove file', self)
        connect(remove_action.triggered, self.on_remove_entry)
        menu.addAction(remove_action)
        menu.exec_(self.window().create_torrent_files_list.mapToGlobal(pos))
Esempio n. 4
0
class SettingsPage(AddBreadcrumbOnShowMixin, QWidget):
    """
    This class is responsible for displaying and adjusting the settings present in Tribler.
    """
    def __init__(self):
        QWidget.__init__(self)
        self.settings = None
        self.saved_dialog = None
        self.version_history = VersionHistory(get_root_state_directory())

    def initialize_settings_page(self):
        self.window().settings_tab.initialize()
        connect(self.window().settings_tab.clicked_tab_button,
                self.clicked_tab_button)
        connect(self.window().settings_save_button.clicked, self.save_settings)

        connect(self.window().download_location_chooser_button.clicked,
                self.on_choose_download_dir_clicked)
        connect(self.window().watch_folder_chooser_button.clicked,
                self.on_choose_watch_dir_clicked)

        connect(self.window().channel_autocommit_checkbox.stateChanged,
                self.on_channel_autocommit_checkbox_changed)
        connect(self.window().family_filter_checkbox.stateChanged,
                self.on_family_filter_checkbox_changed)
        connect(self.window().developer_mode_enabled_checkbox.stateChanged,
                self.on_developer_mode_checkbox_changed)
        connect(self.window().use_monochrome_icon_checkbox.stateChanged,
                self.on_use_monochrome_icon_checkbox_changed)
        connect(self.window().download_settings_anon_checkbox.stateChanged,
                self.on_anon_download_state_changed)
        connect(self.window().log_location_chooser_button.clicked,
                self.on_choose_log_dir_clicked)
        connect(self.window().btn_remove_old_state_dir.clicked,
                self.on_remove_version_dirs)

        checkbox_style = get_checkbox_style()
        for checkbox in [
                self.window().family_filter_checkbox,
                self.window().channel_autocommit_checkbox,
                self.window().always_ask_location_checkbox,
                self.window().developer_mode_enabled_checkbox,
                self.window().use_monochrome_icon_checkbox,
                self.window().download_settings_anon_checkbox,
                self.window().download_settings_anon_seeding_checkbox,
                self.window().lt_utp_checkbox,
                self.window().watchfolder_enabled_checkbox,
                self.window().allow_exit_node_checkbox,
                self.window().developer_mode_enabled_checkbox,
                self.window().checkbox_enable_network_statistics,
                self.window().checkbox_enable_resource_log,
                self.window().download_settings_add_to_channel_checkbox,
        ]:
            checkbox.setStyleSheet(checkbox_style)

        self.update_stacked_widget_height()

    def on_channel_autocommit_checkbox_changed(self, _):
        self.window().gui_settings.setValue(
            "autocommit_enabled",
            self.window().channel_autocommit_checkbox.isChecked())

    def on_family_filter_checkbox_changed(self, _):
        self.window().gui_settings.setValue(
            "family_filter",
            self.window().family_filter_checkbox.isChecked())

    def on_developer_mode_checkbox_changed(self, _):
        self.window().gui_settings.setValue(
            "debug",
            self.window().developer_mode_enabled_checkbox.isChecked())
        self.window().left_menu_button_debug.setHidden(
            not self.window().developer_mode_enabled_checkbox.isChecked())

    def on_use_monochrome_icon_checkbox_changed(self, _):
        use_monochrome_icon = self.window(
        ).use_monochrome_icon_checkbox.isChecked()
        self.window().gui_settings.setValue("use_monochrome_icon",
                                            use_monochrome_icon)
        self.window().update_tray_icon(use_monochrome_icon)

    def on_anon_download_state_changed(self, _):
        if self.window().download_settings_anon_checkbox.isChecked():
            self.window().download_settings_anon_seeding_checkbox.setChecked(
                True)
        self.window().download_settings_anon_seeding_checkbox.setEnabled(
            not self.window().download_settings_anon_checkbox.isChecked())

    def on_choose_download_dir_clicked(self, checked):
        previous_download_path = self.window().download_location_input.text(
        ) or ""
        download_dir = QFileDialog.getExistingDirectory(
            self.window(), "Please select the download location",
            previous_download_path, QFileDialog.ShowDirsOnly)

        if not download_dir:
            return

        self.window().download_location_input.setText(download_dir)

    def on_choose_watch_dir_clicked(self, checked):
        if self.window().watchfolder_enabled_checkbox.isChecked():
            previous_watch_dir = self.window().watchfolder_location_input.text(
            ) or ""
            watch_dir = QFileDialog.getExistingDirectory(
                self.window(), "Please select the watch folder",
                previous_watch_dir, QFileDialog.ShowDirsOnly)

            if not watch_dir:
                return

            self.window().watchfolder_location_input.setText(watch_dir)

    def on_choose_log_dir_clicked(self, checked):
        previous_log_dir = self.window().log_location_input.text() or ""
        log_dir = QFileDialog.getExistingDirectory(
            self.window(), tr("Please select the log directory"),
            previous_log_dir, QFileDialog.ShowDirsOnly)

        if not log_dir or log_dir == previous_log_dir:
            return

        is_writable, error = is_dir_writable(log_dir)
        if not is_writable:
            gui_error_message = f"<i>{log_dir}</i> is not writable. [{error}]"
            ConfirmationDialog.show_message(self.window(),
                                            tr("Insufficient Permissions"),
                                            gui_error_message, "OK")
        else:
            self.window().log_location_input.setText(log_dir)

    def initialize_with_settings(self, settings):
        if not settings:
            return
        self.settings = settings = settings["settings"]
        gui_settings = self.window().gui_settings

        self.window().settings_stacked_widget.show()
        self.window().settings_tab.show()
        self.window().settings_save_button.show()

        # General settings
        self.window().family_filter_checkbox.setChecked(
            get_gui_setting(gui_settings, 'family_filter', True, is_bool=True))
        self.window().use_monochrome_icon_checkbox.setChecked(
            get_gui_setting(gui_settings,
                            "use_monochrome_icon",
                            False,
                            is_bool=True))
        self.window().download_location_input.setText(
            settings['download_defaults']['saveas'])
        self.window().always_ask_location_checkbox.setChecked(
            get_gui_setting(gui_settings,
                            "ask_download_settings",
                            True,
                            is_bool=True))
        self.window().download_settings_anon_checkbox.setChecked(
            settings['download_defaults']['anonymity_enabled'])
        self.window().download_settings_anon_seeding_checkbox.setChecked(
            settings['download_defaults']['safeseeding_enabled'])
        self.window().download_settings_add_to_channel_checkbox.setChecked(
            settings['download_defaults']['add_download_to_channel'])
        self.window().watchfolder_enabled_checkbox.setChecked(
            settings['watch_folder']['enabled'])
        self.window().watchfolder_location_input.setText(
            settings['watch_folder']['directory'])

        # Channel settings
        self.window().channel_autocommit_checkbox.setChecked(
            get_gui_setting(gui_settings,
                            "autocommit_enabled",
                            True,
                            is_bool=True))

        # Log directory
        self.window().log_location_input.setText(
            settings['general']['log_dir'])

        # Connection settings
        self.window().lt_proxy_type_combobox.setCurrentIndex(
            settings['libtorrent']['proxy_type'])
        if settings['libtorrent']['proxy_server']:
            proxy_server = settings['libtorrent']['proxy_server'].split(":")
            self.window().lt_proxy_server_input.setText(proxy_server[0])
            self.window().lt_proxy_port_input.setText(proxy_server[1])
        if settings['libtorrent']['proxy_auth']:
            proxy_auth = settings['libtorrent']['proxy_auth'].split(":")
            self.window().lt_proxy_username_input.setText(proxy_auth[0])
            self.window().lt_proxy_password_input.setText(proxy_auth[1])
        self.window().lt_utp_checkbox.setChecked(settings['libtorrent']['utp'])

        max_conn_download = settings['libtorrent']['max_connections_download']
        if max_conn_download == -1:
            max_conn_download = 0
        self.window().max_connections_download_input.setText(
            str(max_conn_download))

        self.window().api_port_input.setText(
            f"{get_gui_setting(gui_settings, 'api_port', DEFAULT_API_PORT)}")

        # Bandwidth settings
        self.window().upload_rate_limit_input.setText(
            str(settings['libtorrent']['max_upload_rate'] // 1024))
        self.window().download_rate_limit_input.setText(
            str(settings['libtorrent']['max_download_rate'] // 1024))

        # Seeding settings
        getattr(
            self.window(),
            "seeding_" + settings['download_defaults']['seeding_mode'] +
            "_radio").setChecked(True)
        self.window().seeding_time_input.setText(
            seconds_to_hhmm_string(
                settings['download_defaults']['seeding_time']))
        ind = self.window().seeding_ratio_combobox.findText(
            str(settings['download_defaults']['seeding_ratio']))
        if ind != -1:
            self.window().seeding_ratio_combobox.setCurrentIndex(ind)

        # Anonymity settings
        self.window().allow_exit_node_checkbox.setChecked(
            settings['tunnel_community']['exitnode_enabled'])
        self.window().number_hops_slider.setValue(
            int(settings['download_defaults']['number_hops']))
        connect(self.window().number_hops_slider.valueChanged,
                self.update_anonymity_cost_label)
        self.update_anonymity_cost_label(
            int(settings['download_defaults']['number_hops']))

        # Data settings
        self.load_settings_data_tab()

        # Debug
        self.window().developer_mode_enabled_checkbox.setChecked(
            get_gui_setting(gui_settings, "debug", False, is_bool=True))
        self.window().checkbox_enable_resource_log.setChecked(
            settings['resource_monitor']['enabled'])

        cpu_priority = 1
        if 'cpu_priority' in settings['resource_monitor']:
            cpu_priority = int(settings['resource_monitor']['cpu_priority'])
        self.window().slider_cpu_level.setValue(cpu_priority)
        self.window().cpu_priority_value.setText(
            f"Current Priority = {cpu_priority}")
        connect(self.window().slider_cpu_level.valueChanged,
                self.show_updated_cpu_priority)
        self.window().checkbox_enable_network_statistics.setChecked(
            settings['ipv8']['statistics'])

    def _version_dir_checkbox(self, state_dir, enabled=True):
        dir_size = sum(f.stat().st_size for f in state_dir.glob('**/*'))
        text = f"{state_dir}   {format_size(dir_size)}"
        checkbox = QCheckBox(text)
        checkbox.setEnabled(enabled)
        return checkbox

    def load_settings_data_tab(self):
        self.refresh_current_version_checkbox()
        self.refresh_old_version_checkboxes()

    def refresh_current_version_checkbox(self):
        get_root_state_directory()
        code_version_dir = self.version_history.code_version.directory
        self.refresh_version_checkboxes(self.window().state_dir_current,
                                        [code_version_dir],
                                        enabled=False)

    def refresh_old_version_checkboxes(self):
        get_root_state_directory()
        old_state_dirs = self.version_history.get_disposable_state_directories(
        )
        self.refresh_version_checkboxes(self.window().state_dir_list,
                                        old_state_dirs,
                                        enabled=True)

    def refresh_version_checkboxes(self, parent, old_state_dirs, enabled=True):
        version_dirs_layout = parent.layout()
        for checkbox in parent.findChildren(QCheckBox):
            version_dirs_layout.removeWidget(checkbox)
            checkbox.setParent(None)
            checkbox.deleteLater()

        for state_dir in old_state_dirs:
            version_dir_checkbox = self._version_dir_checkbox(state_dir,
                                                              enabled=enabled)
            version_dirs_layout.addWidget(version_dir_checkbox)

    def on_remove_version_dirs(self, _):
        root_version_dir = str(get_root_state_directory())

        def dir_from_checkbox_text(checkbox):
            # eg text: "/home/<user>/.Tribler/v7.8   5 GB"
            state_dir = checkbox.text().rpartition("   ")[0]
            if not state_dir.startswith(root_version_dir):
                return None  # safety check just for case
            state_dir = state_dir[len(root_version_dir):]
            if state_dir.startswith('/'):
                state_dir = state_dir[1:]
            return state_dir

        dirs_selected_for_deletion = []
        for checkbox in self.window().state_dir_list.findChildren(QCheckBox):
            if checkbox.isChecked():
                state_dir = dir_from_checkbox_text(checkbox)
                if state_dir:
                    dirs_selected_for_deletion.append(state_dir)

        if self.on_confirm_remove_version_dirs(dirs_selected_for_deletion):
            remove_state_dirs(root_version_dir, dirs_selected_for_deletion)
            self.refresh_old_version_checkboxes()

    def on_confirm_remove_version_dirs(self, selected_versions):
        message_box = QMessageBox()
        message_box.setIcon(QMessageBox.Question)

        if selected_versions:
            version_dirs_str = "\n- ".join(selected_versions)
            versions_info = tr("Versions: \n- %s") % version_dirs_str

            title = tr("Confirm delete older versions?")
            message_body = (tr("Are you sure to remove the selected versions? "
                               "\nYou can not undo this action."
                               "\n\n ") % versions_info)
            message_buttons = QMessageBox.No | QMessageBox.Yes
        else:
            title = tr("No versions selected")
            message_body = tr("Select a version to delete.")
            message_buttons = QMessageBox.Close

        message_box.setWindowTitle(title)
        message_box.setText(message_body)
        message_box.setStandardButtons(message_buttons)

        user_choice = message_box.exec_()
        return user_choice == QMessageBox.Yes

    def update_anonymity_cost_label(self, value):
        html_text = tr(
            "<html><head/><body><p>Download with <b>%d</b> hop(s) of anonymity. "
            "When you download a file of 200 Megabyte, you will pay roughly <b>%d</b>"
            "Megabyte of bandwidth tokens.</p></body></html>") % (
                value,
                400 * (value - 1) + 200,
            )
        self.window().anonymity_costs_label.setText(html_text)

    def show_updated_cpu_priority(self, value):
        self.window().cpu_priority_value.setText(
            tr("Current Priority = %s") % value)

    def load_settings(self):
        self.window().settings_stacked_widget.hide()
        self.window().settings_tab.hide()
        self.window().settings_save_button.hide()

        TriblerNetworkRequest("settings", self.initialize_with_settings)

    def clicked_tab_button(self, tab_button_name):
        if tab_button_name == "settings_general_button":
            self.window().settings_stacked_widget.setCurrentIndex(
                PAGE_SETTINGS_GENERAL)
        elif tab_button_name == "settings_connection_button":
            self.window().settings_stacked_widget.setCurrentIndex(
                PAGE_SETTINGS_CONNECTION)
        elif tab_button_name == "settings_bandwidth_button":
            self.window().settings_stacked_widget.setCurrentIndex(
                PAGE_SETTINGS_BANDWIDTH)
        elif tab_button_name == "settings_seeding_button":
            self.window().settings_stacked_widget.setCurrentIndex(
                PAGE_SETTINGS_SEEDING)
        elif tab_button_name == "settings_anonymity_button":
            self.window().settings_stacked_widget.setCurrentIndex(
                PAGE_SETTINGS_ANONYMITY)
        elif tab_button_name == "settings_data_button":
            self.window().settings_stacked_widget.setCurrentIndex(
                PAGE_SETTINGS_DATA)
        elif tab_button_name == "settings_debug_button":
            self.window().settings_stacked_widget.setCurrentIndex(
                PAGE_SETTINGS_DEBUG)

        self.update_stacked_widget_height()

    def update_stacked_widget_height(self):
        """
        Update the height of the settings tab. This is required since the height of a QStackedWidget is by default
        the height of the largest page. This messes up the scroll bar.
        """
        for index in range(self.window().settings_stacked_widget.count()):
            if index == self.window().settings_stacked_widget.currentIndex():
                self.window().settings_stacked_widget.setSizePolicy(
                    QSizePolicy.Preferred, QSizePolicy.Preferred)
            else:
                self.window().settings_stacked_widget.setSizePolicy(
                    QSizePolicy.Ignored, QSizePolicy.Ignored)

        self.window().settings_stacked_widget.adjustSize()

    def save_settings(self, checked):
        # Create a dictionary with all available settings
        settings_data = {
            'general': {},
            'Tribler': {},
            'download_defaults': {},
            'libtorrent': {},
            'watch_folder': {},
            'tunnel_community': {},
            'trustchain': {},
            'resource_monitor': {},
            'ipv8': {},
            'chant': {},
        }
        settings_data['download_defaults']['saveas'] = self.window(
        ).download_location_input.text()
        settings_data['general']['log_dir'] = self.window(
        ).log_location_input.text()

        settings_data['watch_folder']['enabled'] = self.window(
        ).watchfolder_enabled_checkbox.isChecked()
        if settings_data['watch_folder']['enabled']:
            settings_data['watch_folder']['directory'] = self.window(
            ).watchfolder_location_input.text()

        settings_data['libtorrent']['proxy_type'] = self.window(
        ).lt_proxy_type_combobox.currentIndex()

        if (self.window().lt_proxy_server_input.text()
                and len(self.window().lt_proxy_server_input.text()) > 0
                and len(self.window().lt_proxy_port_input.text()) > 0):
            try:
                settings_data['libtorrent']['proxy_server'] = "{}:{}".format(
                    self.window().lt_proxy_server_input.text(),
                    int(self.window().lt_proxy_port_input.text()),
                )
            except ValueError:
                ConfirmationDialog.show_error(
                    self.window(),
                    tr("Invalid proxy port number"),
                    tr("You've entered an invalid format for the proxy port number. Please enter a whole number."
                       ),
                )
                return
        else:
            settings_data['libtorrent']['proxy_server'] = ":"

        if self.window().lt_proxy_username_input.text() and self.window(
        ).lt_proxy_password_input.text():
            settings_data['libtorrent']['proxy_auth'] = "{}:{}".format(
                self.window().lt_proxy_username_input.text(),
                self.window().lt_proxy_password_input.text(),
            )
        else:
            settings_data['libtorrent']['proxy_auth'] = ":"

        settings_data['libtorrent']['utp'] = self.window(
        ).lt_utp_checkbox.isChecked()

        try:
            max_conn_download = int(
                self.window().max_connections_download_input.text())
        except ValueError:
            ConfirmationDialog.show_error(
                self.window(),
                tr("Invalid number of connections"),
                tr("You've entered an invalid format for the maximum number of connections. "
                   "Please enter a whole number."),
            )
            return
        if max_conn_download == 0:
            max_conn_download = -1
        settings_data['libtorrent'][
            'max_connections_download'] = max_conn_download

        try:
            if self.window().upload_rate_limit_input.text():
                user_upload_rate_limit = int(
                    float(self.window().upload_rate_limit_input.text()) * 1024)
                if user_upload_rate_limit < MAX_LIBTORRENT_RATE_LIMIT:
                    settings_data['libtorrent'][
                        'max_upload_rate'] = user_upload_rate_limit
                else:
                    raise ValueError
            if self.window().download_rate_limit_input.text():
                user_download_rate_limit = int(
                    float(self.window().download_rate_limit_input.text()) *
                    1024)
                if user_download_rate_limit < MAX_LIBTORRENT_RATE_LIMIT:
                    settings_data['libtorrent'][
                        'max_download_rate'] = user_download_rate_limit
                else:
                    raise ValueError
        except ValueError:
            ConfirmationDialog.show_error(
                self.window(),
                tr("Invalid value for bandwidth limit"),
                tr("You've entered an invalid value for the maximum upload/download rate. \n"
                   "The rate is specified in KB/s and the value permitted is between 0 and %d KB/s.\n"
                   "Note that the decimal values are truncated.") %
                (MAX_LIBTORRENT_RATE_LIMIT / 1024),
            )
            return

        try:
            if self.window().api_port_input.text():
                api_port = int(self.window().api_port_input.text())
                if api_port <= 0 or api_port >= 65536:
                    raise ValueError()
                self.window().gui_settings.setValue("api_port", api_port)
        except ValueError:
            ConfirmationDialog.show_error(
                self.window(),
                tr("Invalid value for api port"),
                tr("Please enter a valid port for the api (between 0 and 65536)"
                   ),
            )
            return

        seeding_modes = ['forever', 'time', 'never', 'ratio']
        selected_mode = 'forever'
        for seeding_mode in seeding_modes:
            if getattr(self.window(),
                       "seeding_" + seeding_mode + "_radio").isChecked():
                selected_mode = seeding_mode
                break
        settings_data['download_defaults']['seeding_mode'] = selected_mode
        settings_data['download_defaults']['seeding_ratio'] = float(
            self.window().seeding_ratio_combobox.currentText())

        try:
            settings_data['download_defaults'][
                'seeding_time'] = string_to_seconds(
                    self.window().seeding_time_input.text())
        except ValueError:
            ConfirmationDialog.show_error(
                self.window(),
                tr("Invalid seeding time"),
                tr("You've entered an invalid format for the seeding time (expected HH:MM)"
                   ),
            )
            return

        settings_data['tunnel_community']['exitnode_enabled'] = self.window(
        ).allow_exit_node_checkbox.isChecked()
        settings_data['download_defaults']['number_hops'] = self.window(
        ).number_hops_slider.value()
        settings_data['download_defaults']['anonymity_enabled'] = self.window(
        ).download_settings_anon_checkbox.isChecked()
        settings_data['download_defaults'][
            'safeseeding_enabled'] = self.window(
            ).download_settings_anon_seeding_checkbox.isChecked()
        settings_data['download_defaults'][
            'add_download_to_channel'] = self.window(
            ).download_settings_add_to_channel_checkbox.isChecked()

        settings_data['resource_monitor']['enabled'] = self.window(
        ).checkbox_enable_resource_log.isChecked()
        settings_data['resource_monitor']['cpu_priority'] = int(
            self.window().slider_cpu_level.value())

        # network statistics
        settings_data['ipv8']['statistics'] = self.window(
        ).checkbox_enable_network_statistics.isChecked()

        self.window().settings_save_button.setEnabled(False)

        # TODO: do it in RESTful style, on the REST return JSON instead
        # In case the default save dir has changed, add it to the top of the list of last download locations.
        # Otherwise, the user could absentmindedly click through the download dialog and start downloading into
        # the last used download dir, and not into the newly designated default download dir.
        if self.settings['download_defaults']['saveas'] != settings_data[
                'download_defaults']['saveas']:
            self.window().update_recent_download_locations(
                settings_data['download_defaults']['saveas'])
        self.settings = settings_data

        TriblerNetworkRequest("settings",
                              self.on_settings_saved,
                              method='POST',
                              raw_data=json.dumps(settings_data))

    def on_settings_saved(self, data):
        if not data:
            return
        # Now save the GUI settings
        self.window().gui_settings.setValue(
            "family_filter",
            self.window().family_filter_checkbox.isChecked())
        self.window().gui_settings.setValue(
            "autocommit_enabled",
            self.window().channel_autocommit_checkbox.isChecked())
        self.window().gui_settings.setValue(
            "ask_download_settings",
            self.window().always_ask_location_checkbox.isChecked())
        self.window().gui_settings.setValue(
            "use_monochrome_icon",
            self.window().use_monochrome_icon_checkbox.isChecked())

        self.saved_dialog = ConfirmationDialog(
            TriblerRequestManager.window,
            tr("Settings saved"),
            tr("Your settings have been saved."),
            [(tr("CLOSE"), BUTTON_TYPE_NORMAL)],
        )
        connect(self.saved_dialog.button_clicked,
                self.on_dialog_cancel_clicked)
        self.saved_dialog.show()
        self.window().fetch_settings()

    def on_dialog_cancel_clicked(self, _):
        self.window().settings_save_button.setEnabled(True)
        self.saved_dialog.close_dialog()
        self.saved_dialog = None
Esempio n. 5
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)
Esempio n. 6
0
class MarketOrdersPage(QWidget):
    """
    This page displays orders in the decentralized market in Tribler.
    """
    def __init__(self):
        QWidget.__init__(self)
        self.initialized = False
        self.selected_item = None
        self.dialog = None
        self.wallets = {}

    def initialize_orders_page(self, wallets):
        if not self.initialized:
            self.window().orders_back_button.setIcon(
                QIcon(get_image_path('page_back.png')))
            self.window().market_orders_list.sortItems(0, Qt.AscendingOrder)
            self.window(
            ).market_orders_list.customContextMenuRequested.connect(
                self.on_right_click_order)
            self.initialized = True

        self.wallets = wallets

        self.load_orders()

    def load_orders(self):
        self.window().market_orders_list.clear()

        TriblerNetworkRequest("market/orders", self.on_received_orders)

    def on_received_orders(self, orders):
        if not orders or not self.wallets:
            return
        for order in orders["orders"]:
            if self.has_valid_order_amount(order):
                asset1_prec = self.wallets[order["assets"]["first"]
                                           ["type"]]["precision"]
                asset2_prec = self.wallets[order["assets"]["second"]
                                           ["type"]]["precision"]
                item = OrderWidgetItem(self.window().market_orders_list, order,
                                       asset1_prec, asset2_prec)
                self.window().market_orders_list.addTopLevelItem(item)

    def on_right_click_order(self, pos):
        item_clicked = self.window().market_orders_list.itemAt(pos)
        if not item_clicked:
            return

        self.selected_item = item_clicked

        if self.selected_item.order[
                'status'] == 'open':  # We can only cancel an open order
            menu = TriblerActionMenu(self)
            cancel_action = QAction('Cancel order', self)
            cancel_action.triggered.connect(self.on_cancel_order_clicked)
            menu.addAction(cancel_action)
            menu.exec_(self.window().market_orders_list.mapToGlobal(pos))

    def on_cancel_order_clicked(self):
        self.dialog = ConfirmationDialog(
            self,
            "Cancel order",
            "Are you sure you want to cancel the order with id %s?" %
            self.selected_item.order['order_number'],
            [('NO', BUTTON_TYPE_NORMAL), ('YES', BUTTON_TYPE_CONFIRM)],
        )
        self.dialog.button_clicked.connect(self.on_confirm_cancel_order)
        self.dialog.show()

    def on_confirm_cancel_order(self, action):
        if action == 1:
            TriblerNetworkRequest(
                "market/orders/%s/cancel" %
                self.selected_item.order['order_number'],
                self.on_order_cancelled,
                method='POST',
            )

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

    def on_order_cancelled(self, response):
        if not response:
            return
        self.load_orders()

    def has_valid_order_amount(self, order):
        return order["assets"]["first"]["amount"] > 0 and order["assets"][
            "second"]["amount"] > 0
Esempio n. 7
0
class DownloadsPage(QWidget):
    """
    This class is responsible for managing all items on the downloads page.
    The downloads page shows all downloads and specific details about a download.
    """

    received_downloads = pyqtSignal(object)

    def __init__(self):
        QWidget.__init__(self)
        self.export_dir = None
        self.filter = DOWNLOADS_FILTER_ALL
        self.download_widgets = {}  # key: infohash, value: QTreeWidgetItem
        self.downloads = None
        self.downloads_timer = QTimer()
        self.downloads_timeout_timer = QTimer()
        self.downloads_last_update = 0
        self.selected_items = []
        self.dialog = None
        self.loading_message_widget = None
        self.total_download = 0
        self.total_upload = 0

        self.rest_request = None

    def showEvent(self, QShowEvent):
        """
        When the downloads tab is clicked, we want to update the downloads list immediately.
        """
        super(DownloadsPage, self).showEvent(QShowEvent)
        self.stop_loading_downloads()
        self.schedule_downloads_timer(True)

    def initialize_downloads_page(self):
        self.window().downloads_tab.initialize()
        self.window().downloads_tab.clicked_tab_button.connect(
            self.on_downloads_tab_button_clicked)

        self.window().start_download_button.clicked.connect(
            self.on_start_download_clicked)
        self.window().stop_download_button.clicked.connect(
            self.on_stop_download_clicked)
        self.window().remove_download_button.clicked.connect(
            self.on_remove_download_clicked)
        self.window().play_download_button.clicked.connect(
            self.on_play_download_clicked)

        self.window().downloads_list.itemSelectionChanged.connect(
            self.on_download_item_clicked)

        self.window().downloads_list.customContextMenuRequested.connect(
            self.on_right_click_item)

        self.window().download_details_widget.initialize_details_widget()
        self.window().download_details_widget.hide()

        self.window().downloads_filter_input.textChanged.connect(
            self.on_filter_text_changed)

        self.window().downloads_list.header().setSortIndicator(
            12, Qt.AscendingOrder)
        self.window().downloads_list.header().resizeSection(12, 146)

        if not self.window().vlc_available:
            self.window().play_download_button.setHidden(True)

    def on_filter_text_changed(self, text):
        self.window().downloads_list.clearSelection()
        self.window().download_details_widget.hide()
        self.update_download_visibility()

    def start_loading_downloads(self):
        self.window().downloads_list.setSelectionMode(
            QAbstractItemView.NoSelection)
        self.loading_message_widget = QTreeWidgetItem()
        self.window().downloads_list.addTopLevelItem(
            self.loading_message_widget)
        self.window().downloads_list.setItemWidget(
            self.loading_message_widget, 2,
            LoadingListItem(self.window().downloads_list))
        self.schedule_downloads_timer(now=True)

    def schedule_downloads_timer(self, now=False):
        self.downloads_timer = QTimer()
        self.downloads_timer.setSingleShot(True)
        self.downloads_timer.timeout.connect(self.load_downloads)
        self.downloads_timer.start(0 if now else 1000)

        self.downloads_timeout_timer = QTimer()
        self.downloads_timeout_timer.setSingleShot(True)
        self.downloads_timeout_timer.timeout.connect(
            self.on_downloads_request_timeout)
        self.downloads_timeout_timer.start(16000)

    def on_downloads_request_timeout(self):
        if self.rest_request:
            self.rest_request.cancel_request()
        self.schedule_downloads_timer()

    def stop_loading_downloads(self):
        self.downloads_timer.stop()
        self.downloads_timeout_timer.stop()

    def load_downloads(self):
        url = "downloads?get_pieces=1"
        if self.window().download_details_widget.currentIndex() == 3:
            url += "&get_peers=1"
        elif self.window().download_details_widget.currentIndex() == 1:
            url += "&get_files=1"

        isactive = not self.isHidden() or self.window(
        ).video_player_page.needsupdate

        if isactive or (time.time() - self.downloads_last_update > 30):
            # Update if the downloads page is visible or if we haven't updated for longer than 30 seconds
            self.downloads_last_update = time.time()
            priority = QNetworkRequest.LowPriority if not isactive else QNetworkRequest.HighPriority
            if self.rest_request:
                self.rest_request.cancel_request()
            self.rest_request = TriblerNetworkRequest(
                url, self.on_received_downloads, priority=priority)

    def on_received_downloads(self, downloads):
        if not downloads or "downloads" not in downloads:
            return  # This might happen when closing Tribler
        loading_widget_index = self.window(
        ).downloads_list.indexOfTopLevelItem(self.loading_message_widget)
        if loading_widget_index > -1:
            self.window().downloads_list.takeTopLevelItem(loading_widget_index)
            self.window().downloads_list.setSelectionMode(
                QAbstractItemView.ExtendedSelection)

        self.downloads = downloads

        self.total_download = 0
        self.total_upload = 0

        download_infohashes = set()

        items = []
        for download in downloads["downloads"]:
            if download["infohash"] in self.download_widgets:
                item = self.download_widgets[download["infohash"]]
            else:
                item = DownloadWidgetItem()
                self.download_widgets[download["infohash"]] = item
                items.append(item)

            item.update_with_download(download)

            # Update video player with download info
            video_infohash = self.window().video_player_page.active_infohash
            if video_infohash != "" and download["infohash"] == video_infohash:
                self.window().video_player_page.update_with_download_info(
                    download)

            self.total_download += download["speed_down"]
            self.total_upload += download["speed_up"]

            download_infohashes.add(download["infohash"])

            if (self.window().download_details_widget.current_download
                    is not None and self.window().download_details_widget.
                    current_download["infohash"] == download["infohash"]):
                self.window(
                ).download_details_widget.current_download = download
                self.window().download_details_widget.update_pages()

        self.window().downloads_list.addTopLevelItems(items)
        for item in items:
            self.window().downloads_list.setItemWidget(item, 2,
                                                       item.bar_container)

        # Check whether there are download that should be removed
        for infohash, item in list(self.download_widgets.items()):
            if infohash not in download_infohashes:
                index = self.window().downloads_list.indexOfTopLevelItem(item)
                self.window().downloads_list.takeTopLevelItem(index)
                del self.download_widgets[infohash]

        self.window().tray_set_tooltip("Down: %s, Up: %s" % (format_speed(
            self.total_download), format_speed(self.total_upload)))
        self.update_download_visibility()
        self.schedule_downloads_timer()

        # Update the top download management button if we have a row selected
        if len(self.window().downloads_list.selectedItems()) > 0:
            self.on_download_item_clicked()

        self.received_downloads.emit(downloads)

    def update_download_visibility(self):
        for i in range(self.window().downloads_list.topLevelItemCount()):
            item = self.window().downloads_list.topLevelItem(i)
            if not isinstance(item, DownloadWidgetItem):
                continue

            filter_match = self.window().downloads_filter_input.text().lower(
            ) in item.download_info["name"].lower()
            is_channel = item.download_info["channel_download"]
            if self.filter == DOWNLOADS_FILTER_CHANNELS:
                item.setHidden(not is_channel or not filter_match)
            else:
                item.setHidden(not item.get_raw_download_status()
                               in DOWNLOADS_FILTER_DEFINITION[self.filter]
                               or not filter_match or is_channel)

    def on_downloads_tab_button_clicked(self, button_name):
        self.filter = button_name2filter[button_name]

        self.window().downloads_list.clearSelection()
        self.window().download_details_widget.hide()
        self.update_download_visibility()

    @staticmethod
    def start_download_enabled(download_widgets):
        return any([
            download_widget.get_raw_download_status() == DLSTATUS_STOPPED
            for download_widget in download_widgets
        ])

    @staticmethod
    def stop_download_enabled(download_widgets):
        return any([
            download_widget.get_raw_download_status()
            not in [DLSTATUS_STOPPED, DLSTATUS_STOPPED_ON_ERROR]
            for download_widget in download_widgets
        ])

    @staticmethod
    def force_recheck_download_enabled(download_widgets):
        return any([
            download_widget.get_raw_download_status() not in [
                DLSTATUS_METADATA, DLSTATUS_HASHCHECKING,
                DLSTATUS_WAITING4HASHCHECK
            ] for download_widget in download_widgets
        ])

    def on_download_item_clicked(self):
        selected_count = len(self.window().downloads_list.selectedItems())
        if selected_count == 0:
            self.window().play_download_button.setEnabled(False)
            self.window().remove_download_button.setEnabled(False)
            self.window().start_download_button.setEnabled(False)
            self.window().stop_download_button.setEnabled(False)
            self.window().download_details_widget.hide()
        elif selected_count == 1:
            self.selected_items = self.window().downloads_list.selectedItems()
            self.window().play_download_button.setEnabled(True)
            self.window().remove_download_button.setEnabled(True)
            self.window().start_download_button.setEnabled(
                DownloadsPage.start_download_enabled(self.selected_items))
            self.window().stop_download_button.setEnabled(
                DownloadsPage.stop_download_enabled(self.selected_items))

            self.window().download_details_widget.update_with_download(
                self.selected_items[0].download_info)
            self.window().download_details_widget.show()
        else:
            self.selected_items = self.window().downloads_list.selectedItems()
            self.window().play_download_button.setEnabled(False)
            self.window().remove_download_button.setEnabled(True)
            self.window().start_download_button.setEnabled(
                DownloadsPage.start_download_enabled(self.selected_items))
            self.window().stop_download_button.setEnabled(
                DownloadsPage.stop_download_enabled(self.selected_items))
            self.window().download_details_widget.hide()

    def on_start_download_clicked(self):
        for selected_item in self.selected_items:
            infohash = selected_item.download_info["infohash"]
            TriblerNetworkRequest("downloads/%s" % infohash,
                                  self.on_download_resumed,
                                  method='PATCH',
                                  data={"state": "resume"})

    def on_download_resumed(self, json_result):
        if json_result and 'modified' in json_result:
            for selected_item in self.selected_items:
                if selected_item.download_info["infohash"] == json_result[
                        "infohash"]:
                    selected_item.download_info[
                        'status'] = "DLSTATUS_DOWNLOADING"
                    selected_item.update_item()
                    self.on_download_item_clicked()

    def on_stop_download_clicked(self):
        for selected_item in self.selected_items:
            infohash = selected_item.download_info["infohash"]
            TriblerNetworkRequest("downloads/%s" % infohash,
                                  self.on_download_stopped,
                                  method='PATCH',
                                  data={"state": "stop"})

    def on_play_download_clicked(self):
        self.window().left_menu_button_video_player.click()
        selected_item = self.selected_items[:1]
        if selected_item:
            self.window().video_player_page.play_media_item(
                selected_item[0].download_info["infohash"], -1)

    def on_download_stopped(self, json_result):
        if json_result and "modified" in json_result:
            for selected_item in self.selected_items:
                if selected_item.download_info["infohash"] == json_result[
                        "infohash"]:
                    selected_item.download_info['status'] = "DLSTATUS_STOPPED"
                    selected_item.update_item()
                    self.on_download_item_clicked()
                    if self.window(
                    ).video_player_page.active_infohash == selected_item.download_info[
                            "infohash"]:
                        self.window().video_player_page.reset_player()

    def on_remove_download_clicked(self):
        self.dialog = ConfirmationDialog(
            self,
            "Remove download",
            "Are you sure you want to remove this download?",
            [
                ('remove download', BUTTON_TYPE_NORMAL),
                ('remove download + data', BUTTON_TYPE_NORMAL),
                ('cancel', BUTTON_TYPE_CONFIRM),
            ],
        )
        self.dialog.button_clicked.connect(self.on_remove_download_dialog)
        self.dialog.show()

    def on_remove_download_dialog(self, action):
        if action != 2:
            for selected_item in self.selected_items:
                infohash = selected_item.download_info["infohash"]

                # Reset video player if necessary before doing the actual request
                if self.window().video_player_page.active_infohash == infohash:
                    self.window().video_player_page.reset_player()

                TriblerNetworkRequest(
                    "downloads/%s" % infohash,
                    self.on_download_removed,
                    method='DELETE',
                    data={"remove_data": bool(action)},
                )
        if self.dialog:
            self.dialog.close_dialog()
            self.dialog = None

    def on_download_removed(self, json_result):
        if json_result and "removed" in json_result:
            self.load_downloads()
            self.window().download_details_widget.hide()

    def on_force_recheck_download(self):
        for selected_item in self.selected_items:
            infohash = selected_item.download_info["infohash"]
            TriblerNetworkRequest("downloads/%s" % infohash,
                                  self.on_forced_recheck,
                                  method='PATCH',
                                  data={"state": "recheck"})

    def on_forced_recheck(self, result):
        if result and "modified" in result:
            for selected_item in self.selected_items:
                if selected_item.download_info["infohash"] == result[
                        "infohash"]:
                    selected_item.download_info[
                        'status'] = "DLSTATUS_HASHCHECKING"
                    selected_item.update_item()
                    self.on_download_item_clicked()

    def on_change_anonymity(self, result):
        if result and "modified" in result:
            if result["infohash"] == self.window(
            ).video_player_page.active_infohash:
                self.window().video_player_page.reset_player()

    def change_anonymity(self, hops):
        for selected_item in self.selected_items:
            infohash = selected_item.download_info["infohash"]
            TriblerNetworkRequest("downloads/%s" % infohash,
                                  self.on_change_anonymity,
                                  method='PATCH',
                                  data={"anon_hops": hops})

    def on_explore_files(self):
        for selected_item in self.selected_items:
            path = os.path.normpath(selected_item.download_info["destination"])
            QDesktopServices.openUrl(QUrl.fromLocalFile(path))

    def on_move_files(self):
        if len(self.selected_items) != 1:
            return

        dest_dir = QFileDialog.getExistingDirectory(
            self,
            "Please select the destination directory",
            self.selected_items[0].download_info["destination"],
            QFileDialog.ShowDirsOnly,
        )
        if not dest_dir:
            return

        _infohash = self.selected_items[0].download_info["infohash"]
        _name = self.selected_items[0].download_info["name"]

        data = {"state": "move_storage", "dest_dir": dest_dir}

        TriblerNetworkRequest(
            "downloads/%s" % _infohash,
            lambda res, _, name=_name, target=dest_dir: self.on_files_moved(
                res, name, target),
            data=data,
            method='PATCH',
        )

    def on_files_moved(self, response, name, dest_dir):
        if "modified" in response and response["modified"]:
            self.window().tray_show_message(name, "Moved to %s" % dest_dir)

    def on_export_download(self):
        self.export_dir = QFileDialog.getExistingDirectory(
            self, "Please select the destination directory", "",
            QFileDialog.ShowDirsOnly)

        selected_item = self.selected_items[:1]
        if len(self.export_dir) > 0 and selected_item:
            # Show confirmation dialog where we specify the name of the file
            torrent_name = selected_item[0].download_info['name']
            self.dialog = ConfirmationDialog(
                self,
                "Export torrent file",
                "Please enter the name of the torrent file:",
                [('SAVE', BUTTON_TYPE_NORMAL),
                 ('CANCEL', BUTTON_TYPE_CONFIRM)],
                show_input=True,
            )
            self.dialog.dialog_widget.dialog_input.setPlaceholderText(
                'Torrent file name')
            self.dialog.dialog_widget.dialog_input.setText("%s.torrent" %
                                                           torrent_name)
            self.dialog.dialog_widget.dialog_input.setFocus()
            self.dialog.button_clicked.connect(
                self.on_export_download_dialog_done)
            self.dialog.show()

    def on_export_download_dialog_done(self, action):
        selected_item = self.selected_items[:1]
        if action == 0 and selected_item:
            filename = self.dialog.dialog_widget.dialog_input.text()
            TriblerFileDownloadRequest(
                "downloads/%s/torrent" %
                selected_item[0].download_info['infohash'],
                lambda data, _: self.on_export_download_request_done(
                    filename, data),
            )

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

    def on_export_download_request_done(self, filename, data):
        dest_path = os.path.join(self.export_dir, filename)
        try:
            torrent_file = open(dest_path, "wb")
            torrent_file.write(data)
            torrent_file.close()
        except IOError as exc:
            ConfirmationDialog.show_error(
                self.window(),
                "Error when exporting file",
                "An error occurred when exporting the torrent file: %s" %
                str(exc),
            )
        else:
            self.window().tray_show_message(
                "Torrent file exported",
                "Torrent file exported to %s" % dest_path)

    def on_add_to_channel(self):
        def on_add_button_pressed(channel_id):
            for selected_item in self.selected_items:
                infohash = selected_item.download_info["infohash"]
                name = selected_item.download_info["name"]
                TriblerNetworkRequest(
                    f"channels/mychannel/{channel_id}/torrents",
                    lambda _: self.window().tray_show_message(
                        "Channel update", "Torrent(s) added to your channel"),
                    method='PUT',
                    data={"uri": compose_magnetlink(infohash, name=name)},
                )

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

    def on_right_click_item(self, pos):
        item_clicked = self.window().downloads_list.itemAt(pos)
        if not item_clicked or self.selected_items is None:
            return

        if item_clicked not in self.selected_items:
            self.selected_items.append(item_clicked)

        menu = TriblerActionMenu(self)

        start_action = QAction('Start', self)
        stop_action = QAction('Stop', self)
        remove_download_action = QAction('Remove download', self)
        add_to_channel_action = QAction('Add to my channel', self)
        force_recheck_action = QAction('Force recheck', self)
        export_download_action = QAction('Export .torrent file', self)
        explore_files_action = QAction('Explore files', self)
        move_files_action = QAction('Move file storage', self)

        no_anon_action = QAction('No anonymity', self)
        one_hop_anon_action = QAction('One hop', self)
        two_hop_anon_action = QAction('Two hops', self)
        three_hop_anon_action = QAction('Three hops', self)

        start_action.triggered.connect(self.on_start_download_clicked)
        start_action.setEnabled(
            DownloadsPage.start_download_enabled(self.selected_items))
        stop_action.triggered.connect(self.on_stop_download_clicked)
        stop_action.setEnabled(
            DownloadsPage.stop_download_enabled(self.selected_items))
        add_to_channel_action.triggered.connect(self.on_add_to_channel)
        remove_download_action.triggered.connect(
            self.on_remove_download_clicked)
        force_recheck_action.triggered.connect(self.on_force_recheck_download)
        force_recheck_action.setEnabled(
            DownloadsPage.force_recheck_download_enabled(self.selected_items))
        export_download_action.triggered.connect(self.on_export_download)
        explore_files_action.triggered.connect(self.on_explore_files)
        move_files_action.triggered.connect(self.on_move_files)

        no_anon_action.triggered.connect(lambda: self.change_anonymity(0))
        one_hop_anon_action.triggered.connect(lambda: self.change_anonymity(1))
        two_hop_anon_action.triggered.connect(lambda: self.change_anonymity(2))
        three_hop_anon_action.triggered.connect(
            lambda: self.change_anonymity(3))

        menu.addAction(start_action)
        menu.addAction(stop_action)

        if self.window().vlc_available and len(self.selected_items) == 1:
            play_action = QAction('Play', self)
            play_action.triggered.connect(self.on_play_download_clicked)
            menu.addAction(play_action)
        menu.addSeparator()
        menu.addAction(add_to_channel_action)
        menu.addSeparator()
        menu.addAction(remove_download_action)
        menu.addSeparator()
        menu.addAction(force_recheck_action)
        menu.addSeparator()

        exclude_states = [
            DLSTATUS_METADATA,
            DLSTATUS_CIRCUITS,
            DLSTATUS_EXIT_NODES,
            DLSTATUS_HASHCHECKING,
            DLSTATUS_WAITING4HASHCHECK,
        ]
        if len(self.selected_items) == 1 and self.selected_items[
                0].get_raw_download_status() not in exclude_states:
            menu.addAction(export_download_action)
        menu.addAction(explore_files_action)
        if len(self.selected_items) == 1:
            menu.addAction(move_files_action)
        menu.addSeparator()

        menu_anon_level = menu.addMenu("Change Anonymity ")
        menu_anon_level.addAction(no_anon_action)
        menu_anon_level.addAction(one_hop_anon_action)
        menu_anon_level.addAction(two_hop_anon_action)
        menu_anon_level.addAction(three_hop_anon_action)

        menu.exec_(self.window().downloads_list.mapToGlobal(pos))
Esempio n. 8
0
class SettingsPage(QWidget):
    """
    This class is responsible for displaying and adjusting the settings present in Tribler.
    """

    def __init__(self):
        QWidget.__init__(self)
        self.settings = None
        self.saved_dialog = None
        self.empty_tokens_barcode_dialog = None
        self.empty_partial_tokens_dialog = None
        self.confirm_empty_tokens_dialog = None

    def initialize_settings_page(self):
        self.window().settings_tab.initialize()
        self.window().settings_tab.clicked_tab_button.connect(self.clicked_tab_button)
        self.window().settings_save_button.clicked.connect(self.save_settings)

        self.window().download_location_chooser_button.clicked.connect(self.on_choose_download_dir_clicked)
        self.window().watch_folder_chooser_button.clicked.connect(self.on_choose_watch_dir_clicked)

        self.window().channel_autocommit_checkbox.stateChanged.connect(self.on_channel_autocommit_checkbox_changed)
        self.window().family_filter_checkbox.stateChanged.connect(self.on_family_filter_checkbox_changed)
        self.window().developer_mode_enabled_checkbox.stateChanged.connect(self.on_developer_mode_checkbox_changed)
        self.window().use_monochrome_icon_checkbox.stateChanged.connect(self.on_use_monochrome_icon_checkbox_changed)
        self.window().download_settings_anon_checkbox.stateChanged.connect(self.on_anon_download_state_changed)
        self.window().fully_empty_tokens_button.clicked.connect(self.confirm_fully_empty_tokens)
        self.window().partially_empty_tokens_button.clicked.connect(self.partially_empty_tokens)
        self.window().log_location_chooser_button.clicked.connect(self.on_choose_log_dir_clicked)

        checkbox_style = get_checkbox_style()
        for checkbox in [
            self.window().family_filter_checkbox,
            self.window().channel_autocommit_checkbox,
            self.window().always_ask_location_checkbox,
            self.window().developer_mode_enabled_checkbox,
            self.window().use_monochrome_icon_checkbox,
            self.window().download_settings_anon_checkbox,
            self.window().download_settings_anon_seeding_checkbox,
            self.window().lt_utp_checkbox,
            self.window().watchfolder_enabled_checkbox,
            self.window().allow_exit_node_checkbox,
            self.window().developer_mode_enabled_checkbox,
            self.window().checkbox_enable_network_statistics,
            self.window().checkbox_enable_resource_log,
            self.window().download_settings_add_to_channel_checkbox,
        ]:
            checkbox.setStyleSheet(checkbox_style)

        self.update_stacked_widget_height()

    def confirm_fully_empty_tokens(self):
        self.confirm_empty_tokens_dialog = ConfirmationDialog(
            self,
            "Empty tokens into another account",
            "Are you sure you want to empty ALL bandwidth tokens "
            "into another account? "
            "Warning: one-way action that cannot be revered",
            [('EMPTY', BUTTON_TYPE_CONFIRM), ('CANCEL', BUTTON_TYPE_NORMAL)],
        )
        self.confirm_empty_tokens_dialog.button_clicked.connect(self.on_confirm_fully_empty_tokens)
        self.confirm_empty_tokens_dialog.show()

    def on_confirm_fully_empty_tokens(self, action):
        self.confirm_empty_tokens_dialog.close_dialog()
        self.confirm_empty_tokens_dialog = None

        if action == 0:
            TriblerNetworkRequest("trustchain/bootstrap", self.on_emptying_tokens)

    def partially_empty_tokens(self):
        self.empty_partial_tokens_dialog = ConfirmationDialog(
            self,
            "Empty tokens into another account",
            "Specify the amount of bandwidth tokens to empty into " "another account below:",
            [('EMPTY', BUTTON_TYPE_CONFIRM), ('CANCEL', BUTTON_TYPE_NORMAL)],
            show_input=True,
        )
        self.empty_partial_tokens_dialog.dialog_widget.dialog_input.setPlaceholderText(
            'Please enter the amount of tokens in MB'
        )
        self.empty_partial_tokens_dialog.dialog_widget.dialog_input.setFocus()
        self.empty_partial_tokens_dialog.button_clicked.connect(self.confirm_partially_empty_tokens)
        self.empty_partial_tokens_dialog.show()

    def confirm_partially_empty_tokens(self, action):
        tokens = self.empty_partial_tokens_dialog.dialog_widget.dialog_input.text()
        self.empty_partial_tokens_dialog.close_dialog()
        self.empty_partial_tokens_dialog = None

        if action == 0:
            try:
                tokens = int(float(tokens))
            except ValueError:
                ConfirmationDialog.show_error(self.window(), "Wrong input", "The provided amount is not a number")
                return

            self.confirm_empty_tokens_dialog = ConfirmationDialog(
                self,
                "Empty tokens into another account",
                "Are you sure you want to empty %d bandwidth tokens "
                "into another account? "
                "Warning: one-way action that cannot be revered" % tokens,
                [('EMPTY', BUTTON_TYPE_NORMAL), ('CANCEL', BUTTON_TYPE_CONFIRM)],
            )
            self.confirm_empty_tokens_dialog.button_clicked.connect(
                lambda action2: self.on_confirm_partially_empty_tokens(action2, tokens)
            )
            self.confirm_empty_tokens_dialog.show()

    def on_confirm_partially_empty_tokens(self, action, tokens):
        self.confirm_empty_tokens_dialog.close_dialog()
        self.confirm_empty_tokens_dialog = None
        if action == 0:
            TriblerNetworkRequest("trustchain/bootstrap?amount=%d" % (tokens * MEBIBYTE), self.on_emptying_tokens)

    def on_emptying_tokens(self, data):
        if not data:
            return
        json_data = json.dumps(data)

        if has_qr:
            self.empty_tokens_barcode_dialog = QWidget()
            self.empty_tokens_barcode_dialog.setWindowTitle("Please scan the following QR code")
            self.empty_tokens_barcode_dialog.setGeometry(10, 10, 500, 500)
            qr = qrcode.QRCode(version=1, error_correction=qrcode.constants.ERROR_CORRECT_M, box_size=10, border=5)
            qr.add_data(json_data)
            qr.make(fit=True)

            img = qr.make_image()  # PIL format

            qim = ImageQt(img)
            pixmap = QtGui.QPixmap.fromImage(qim).scaled(600, 600, QtCore.Qt.KeepAspectRatio)
            label = QLabel(self.empty_tokens_barcode_dialog)
            label.setPixmap(pixmap)
            self.empty_tokens_barcode_dialog.resize(pixmap.width(), pixmap.height())
            self.empty_tokens_barcode_dialog.show()
        else:
            ConfirmationDialog.show_error(self.window(), DEPENDENCY_ERROR_TITLE, DEPENDENCY_ERROR_MESSAGE)

    def on_channel_autocommit_checkbox_changed(self, _):
        self.window().gui_settings.setValue("autocommit_enabled", self.window().channel_autocommit_checkbox.isChecked())

    def on_family_filter_checkbox_changed(self, _):
        self.window().gui_settings.setValue("family_filter", self.window().family_filter_checkbox.isChecked())

    def on_developer_mode_checkbox_changed(self, _):
        self.window().gui_settings.setValue("debug", self.window().developer_mode_enabled_checkbox.isChecked())
        self.window().left_menu_button_debug.setHidden(not self.window().developer_mode_enabled_checkbox.isChecked())

    def on_use_monochrome_icon_checkbox_changed(self, _):
        use_monochrome_icon = self.window().use_monochrome_icon_checkbox.isChecked()
        self.window().gui_settings.setValue("use_monochrome_icon", use_monochrome_icon)
        self.window().update_tray_icon(use_monochrome_icon)

    def on_anon_download_state_changed(self, _):
        if self.window().download_settings_anon_checkbox.isChecked():
            self.window().download_settings_anon_seeding_checkbox.setChecked(True)
        self.window().download_settings_anon_seeding_checkbox.setEnabled(
            not self.window().download_settings_anon_checkbox.isChecked()
        )

    def on_choose_download_dir_clicked(self):
        previous_download_path = self.window().download_location_input.text() or ""
        download_dir = QFileDialog.getExistingDirectory(
            self.window(), "Please select the download location", previous_download_path, QFileDialog.ShowDirsOnly
        )

        if not download_dir:
            return

        self.window().download_location_input.setText(download_dir)

    def on_choose_watch_dir_clicked(self):
        if self.window().watchfolder_enabled_checkbox.isChecked():
            previous_watch_dir = self.window().watchfolder_location_input.text() or ""
            watch_dir = QFileDialog.getExistingDirectory(
                self.window(), "Please select the watch folder", previous_watch_dir, QFileDialog.ShowDirsOnly
            )

            if not watch_dir:
                return

            self.window().watchfolder_location_input.setText(watch_dir)

    def on_choose_log_dir_clicked(self):
        previous_log_dir = self.window().log_location_input.text() or ""
        log_dir = QFileDialog.getExistingDirectory(
            self.window(), "Please select the log directory", previous_log_dir, QFileDialog.ShowDirsOnly
        )

        if not log_dir or log_dir == previous_log_dir:
            return

        is_writable, error = is_dir_writable(log_dir)
        if not is_writable:
            gui_error_message = "<i>%s</i> is not writable. [%s]" % (log_dir, error)
            ConfirmationDialog.show_message(self.window(), "Insufficient Permissions", gui_error_message, "OK")
        else:
            self.window().log_location_input.setText(log_dir)

    def initialize_with_settings(self, settings):
        if not settings:
            return
        self.settings = settings
        settings = settings["settings"]
        gui_settings = self.window().gui_settings

        self.window().settings_stacked_widget.show()
        self.window().settings_tab.show()
        self.window().settings_save_button.show()

        # General settings
        self.window().family_filter_checkbox.setChecked(
            get_gui_setting(gui_settings, 'family_filter', True, is_bool=True)
        )
        self.window().use_monochrome_icon_checkbox.setChecked(
            get_gui_setting(gui_settings, "use_monochrome_icon", False, is_bool=True)
        )
        self.window().download_location_input.setText(settings['download_defaults']['saveas'])
        self.window().always_ask_location_checkbox.setChecked(
            get_gui_setting(gui_settings, "ask_download_settings", True, is_bool=True)
        )
        self.window().download_settings_anon_checkbox.setChecked(settings['download_defaults']['anonymity_enabled'])
        self.window().download_settings_anon_seeding_checkbox.setChecked(
            settings['download_defaults']['safeseeding_enabled']
        )
        self.window().download_settings_add_to_channel_checkbox.setChecked(
            settings['download_defaults']['add_download_to_channel']
        )
        self.window().watchfolder_enabled_checkbox.setChecked(settings['watch_folder']['enabled'])
        self.window().watchfolder_location_input.setText(settings['watch_folder']['directory'])

        # Channel settings
        self.window().channel_autocommit_checkbox.setChecked(
            get_gui_setting(gui_settings, "autocommit_enabled", True, is_bool=True)
        )

        # Market settings
        self.window().enable_market_checkbox.setChecked(settings['market_community']['enabled'])

        # Log directory
        self.window().log_location_input.setText(settings['general']['log_dir'])

        # Connection settings
        self.window().lt_proxy_type_combobox.setCurrentIndex(settings['libtorrent']['proxy_type'])
        if settings['libtorrent']['proxy_server']:
            proxy_server = settings['libtorrent']['proxy_server'].split(":")
            self.window().lt_proxy_server_input.setText(proxy_server[0])
            self.window().lt_proxy_port_input.setText(proxy_server[1])
        if settings['libtorrent']['proxy_auth']:
            proxy_auth = settings['libtorrent']['proxy_auth'].split(":")
            self.window().lt_proxy_username_input.setText(proxy_auth[0])
            self.window().lt_proxy_password_input.setText(proxy_auth[1])
        self.window().lt_utp_checkbox.setChecked(settings['libtorrent']['utp'])

        max_conn_download = settings['libtorrent']['max_connections_download']
        if max_conn_download == -1:
            max_conn_download = 0
        self.window().max_connections_download_input.setText(str(max_conn_download))

        self.window().api_port_input.setText("%s" % get_gui_setting(gui_settings, "api_port", DEFAULT_API_PORT))

        # Bandwidth settings
        self.window().upload_rate_limit_input.setText(str(settings['libtorrent']['max_upload_rate'] // 1024))
        self.window().download_rate_limit_input.setText(str(settings['libtorrent']['max_download_rate'] // 1024))

        # Seeding settings
        getattr(self.window(), "seeding_" + settings['download_defaults']['seeding_mode'] + "_radio").setChecked(True)
        self.window().seeding_time_input.setText(seconds_to_hhmm_string(settings['download_defaults']['seeding_time']))
        ind = self.window().seeding_ratio_combobox.findText(str(settings['download_defaults']['seeding_ratio']))
        if ind != -1:
            self.window().seeding_ratio_combobox.setCurrentIndex(ind)

        # Anonymity settings
        self.window().allow_exit_node_checkbox.setChecked(settings['tunnel_community']['exitnode_enabled'])
        self.window().number_hops_slider.setValue(int(settings['download_defaults']['number_hops']))
        self.window().number_hops_slider.valueChanged.connect(self.update_anonymity_cost_label)
        self.update_anonymity_cost_label(int(settings['download_defaults']['number_hops']))

        # Debug
        self.window().developer_mode_enabled_checkbox.setChecked(
            get_gui_setting(gui_settings, "debug", False, is_bool=True)
        )
        self.window().checkbox_enable_resource_log.setChecked(settings['resource_monitor']['enabled'])

        cpu_priority = 1
        if 'cpu_priority' in settings['resource_monitor']:
            cpu_priority = int(settings['resource_monitor']['cpu_priority'])
        self.window().slider_cpu_level.setValue(cpu_priority)
        self.window().cpu_priority_value.setText("Current Priority = %s" % cpu_priority)
        self.window().slider_cpu_level.valueChanged.connect(self.show_updated_cpu_priority)
        self.window().checkbox_enable_network_statistics.setChecked(settings['ipv8']['statistics'])

    def update_anonymity_cost_label(self, value):
        html_text = """<html><head/><body><p>Download with <b>%d</b> hop(s) of anonymity. 
        When you download a file of 200 Megabyte, you will pay roughly <b>%d</b>
        Megabyte of bandwidth tokens.</p></body></html>
        """ % (
            value,
            400 * (value - 1) + 200,
        )
        self.window().anonymity_costs_label.setText(html_text)

    def show_updated_cpu_priority(self, value):
        self.window().cpu_priority_value.setText("Current Priority = %s" % value)

    def load_settings(self):
        self.window().settings_stacked_widget.hide()
        self.window().settings_tab.hide()
        self.window().settings_save_button.hide()

        TriblerNetworkRequest("settings", self.initialize_with_settings)

    def clicked_tab_button(self, tab_button_name):
        if tab_button_name == "settings_general_button":
            self.window().settings_stacked_widget.setCurrentIndex(PAGE_SETTINGS_GENERAL)
        elif tab_button_name == "settings_connection_button":
            self.window().settings_stacked_widget.setCurrentIndex(PAGE_SETTINGS_CONNECTION)
        elif tab_button_name == "settings_bandwidth_button":
            self.window().settings_stacked_widget.setCurrentIndex(PAGE_SETTINGS_BANDWIDTH)
        elif tab_button_name == "settings_seeding_button":
            self.window().settings_stacked_widget.setCurrentIndex(PAGE_SETTINGS_SEEDING)
        elif tab_button_name == "settings_anonymity_button":
            self.window().settings_stacked_widget.setCurrentIndex(PAGE_SETTINGS_ANONYMITY)
        elif tab_button_name == "settings_debug_button":
            self.window().settings_stacked_widget.setCurrentIndex(PAGE_SETTINGS_DEBUG)

        self.update_stacked_widget_height()

    def update_stacked_widget_height(self):
        """
        Update the height of the settings tab. This is required since the height of a QStackedWidget is by default
        the height of the largest page. This messes up the scroll bar.
        """
        for index in range(self.window().settings_stacked_widget.count()):
            if index == self.window().settings_stacked_widget.currentIndex():
                self.window().settings_stacked_widget.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred)
            else:
                self.window().settings_stacked_widget.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored)

        self.window().settings_stacked_widget.adjustSize()

    def save_settings(self):
        # Create a dictionary with all available settings
        settings_data = {
            'general': {},
            'Tribler': {},
            'download_defaults': {},
            'libtorrent': {},
            'watch_folder': {},
            'tunnel_community': {},
            'market_community': {},
            'trustchain': {},
            'resource_monitor': {},
            'ipv8': {},
            'chant': {},
        }
        settings_data['download_defaults']['saveas'] = self.window().download_location_input.text()
        settings_data['general']['log_dir'] = self.window().log_location_input.text()

        settings_data['watch_folder']['enabled'] = self.window().watchfolder_enabled_checkbox.isChecked()
        if settings_data['watch_folder']['enabled']:
            settings_data['watch_folder']['directory'] = self.window().watchfolder_location_input.text()

        settings_data['market_community']['enabled'] = self.window().enable_market_checkbox.isChecked()

        settings_data['libtorrent']['proxy_type'] = self.window().lt_proxy_type_combobox.currentIndex()

        if (
            self.window().lt_proxy_server_input.text()
            and len(self.window().lt_proxy_server_input.text()) > 0
            and len(self.window().lt_proxy_port_input.text()) > 0
        ):
            try:
                settings_data['libtorrent']['proxy_server'] = "%s:%s" % (
                    self.window().lt_proxy_server_input.text(),
                    int(self.window().lt_proxy_port_input.text()),
                )
            except ValueError:
                ConfirmationDialog.show_error(
                    self.window(),
                    "Invalid proxy port number",
                    "You've entered an invalid format for the proxy port number. " "Please enter a whole number.",
                )
                return
        else:
            settings_data['libtorrent']['proxy_server'] = ":"

        if self.window().lt_proxy_username_input.text() and self.window().lt_proxy_password_input.text():
            settings_data['libtorrent']['proxy_auth'] = "%s:%s" % (
                self.window().lt_proxy_username_input.text(),
                self.window().lt_proxy_password_input.text(),
            )
        else:
            settings_data['libtorrent']['proxy_auth'] = ":"

        settings_data['libtorrent']['utp'] = self.window().lt_utp_checkbox.isChecked()

        try:
            max_conn_download = int(self.window().max_connections_download_input.text())
        except ValueError:
            ConfirmationDialog.show_error(
                self.window(),
                "Invalid number of connections",
                "You've entered an invalid format for the maximum number of connections. "
                "Please enter a whole number.",
            )
            return
        if max_conn_download == 0:
            max_conn_download = -1
        settings_data['libtorrent']['max_connections_download'] = max_conn_download

        try:
            if self.window().upload_rate_limit_input.text():
                user_upload_rate_limit = int(self.window().upload_rate_limit_input.text()) * 1024
                if user_upload_rate_limit < sys.maxsize:
                    settings_data['libtorrent']['max_upload_rate'] = user_upload_rate_limit
                else:
                    raise ValueError
            if self.window().download_rate_limit_input.text():
                user_download_rate_limit = int(self.window().download_rate_limit_input.text()) * 1024
                if user_download_rate_limit < sys.maxsize:
                    settings_data['libtorrent']['max_download_rate'] = user_download_rate_limit
                else:
                    raise ValueError
        except ValueError:
            ConfirmationDialog.show_error(
                self.window(),
                "Invalid value for bandwidth limit",
                "You've entered an invalid value for the maximum upload/download rate. "
                "Please enter a whole number (max: %d)" % (sys.maxsize / 1000),
            )
            return

        try:
            if self.window().api_port_input.text():
                api_port = int(self.window().api_port_input.text())
                if api_port <= 0 or api_port >= 65536:
                    raise ValueError()
                self.window().gui_settings.setValue("api_port", api_port)
        except ValueError:
            ConfirmationDialog.show_error(
                self.window(),
                "Invalid value for api port",
                "Please enter a valid port for the api (between 0 and 65536)",
            )
            return

        seeding_modes = ['forever', 'time', 'never', 'ratio']
        selected_mode = 'forever'
        for seeding_mode in seeding_modes:
            if getattr(self.window(), "seeding_" + seeding_mode + "_radio").isChecked():
                selected_mode = seeding_mode
                break
        settings_data['download_defaults']['seeding_mode'] = selected_mode
        settings_data['download_defaults']['seeding_ratio'] = float(self.window().seeding_ratio_combobox.currentText())

        try:
            settings_data['download_defaults']['seeding_time'] = string_to_seconds(
                self.window().seeding_time_input.text()
            )
        except ValueError:
            ConfirmationDialog.show_error(
                self.window(),
                "Invalid seeding time",
                "You've entered an invalid format for the seeding time (expected HH:MM)",
            )
            return

        settings_data['tunnel_community']['exitnode_enabled'] = self.window().allow_exit_node_checkbox.isChecked()
        settings_data['download_defaults']['number_hops'] = self.window().number_hops_slider.value()
        settings_data['download_defaults'][
            'anonymity_enabled'
        ] = self.window().download_settings_anon_checkbox.isChecked()
        settings_data['download_defaults'][
            'safeseeding_enabled'
        ] = self.window().download_settings_anon_seeding_checkbox.isChecked()
        settings_data['download_defaults'][
            'add_download_to_channel'
        ] = self.window().download_settings_add_to_channel_checkbox.isChecked()

        settings_data['resource_monitor']['enabled'] = self.window().checkbox_enable_resource_log.isChecked()
        settings_data['resource_monitor']['cpu_priority'] = int(self.window().slider_cpu_level.value())

        # network statistics
        settings_data['ipv8']['statistics'] = self.window().checkbox_enable_network_statistics.isChecked()

        self.window().settings_save_button.setEnabled(False)

        TriblerNetworkRequest("settings", self.on_settings_saved, method='POST', raw_data=json.dumps(settings_data))

    def on_settings_saved(self, data):
        if not data:
            return
        # Now save the GUI settings
        self.window().gui_settings.setValue("family_filter", self.window().family_filter_checkbox.isChecked())
        self.window().gui_settings.setValue("autocommit_enabled", self.window().channel_autocommit_checkbox.isChecked())
        self.window().gui_settings.setValue(
            "ask_download_settings", self.window().always_ask_location_checkbox.isChecked()
        )
        self.window().gui_settings.setValue(
            "use_monochrome_icon", self.window().use_monochrome_icon_checkbox.isChecked()
        )

        self.saved_dialog = ConfirmationDialog(
            TriblerRequestManager.window,
            "Settings saved",
            "Your settings have been saved.",
            [('CLOSE', BUTTON_TYPE_NORMAL)],
        )
        self.saved_dialog.button_clicked.connect(self.on_dialog_cancel_clicked)
        self.saved_dialog.show()
        self.window().fetch_settings()

    def on_dialog_cancel_clicked(self, _):
        self.window().settings_save_button.setEnabled(True)
        self.saved_dialog.close_dialog()
        self.saved_dialog = None
Esempio n. 9
0
class ChannelContentsWidget(AddBreadcrumbOnShowMixin, widget_form,
                            widget_class):
    def __init__(self, parent=None):
        super(widget_class, self).__init__(parent=parent)
        # FIXME!!! This is a dumb workaround for a bug(?) in PyQT bindings in Python 3.7
        # When more than a single instance of a class is created, every next setupUi
        # triggers connectSlotsByName error. There are some reports that it is specific to
        # 3.7 and there is a fix in the 10.08.2019 PyQt bindings snapshot.
        try:
            self.setupUi(self)
        except SystemError:
            pass

        # ! ACHTUNG !
        # There is a bug in PyQT bindings that prevents uic.loadUiType from correctly
        # detecting paths to external resources used in .ui files. Therefore,
        # for each external resource (e.g. image/icon), we must reload it manually here.
        self.channel_options_button.setIcon(
            QIcon(get_image_path('ellipsis.png')))
        self.channel_preview_button.setIcon(
            QIcon(get_image_path('refresh.png')))
        self.channel_preview_button.setToolTip(
            'Click to load preview contents')

        self.default_channel_model = ChannelContentModel

        self.initialized = False
        self.chosen_dir = None
        self.dialog = None
        self.controller = None
        self.commit_timer = None
        self.autocommit_enabled = None
        self.channel_options_menu = None

        self.channels_stack = []

        self_ref = self

        self.hide_xxx = None

        # This context manager is used to freeze the state of controls in the models stack to
        # prevent signals from triggering for inactive models.
        class freeze_controls_class:
            objects_to_freeze = [
                self_ref.category_selector,
                self_ref.content_table.horizontalHeader(),
                self_ref.channel_torrents_filter_input,
            ]

            def __enter__(self):
                for obj in self.objects_to_freeze:
                    obj.blockSignals(True)

            def __exit__(self, *args):
                for obj in self.objects_to_freeze:
                    obj.blockSignals(False)

        self.freeze_controls = freeze_controls_class
        self.setStyleSheet(
            "QToolTip { color: #222222; background-color: #eeeeee; border: 0px; }"
        )

    @property
    def personal_channel_model(self):
        return SimplifiedPersonalChannelsModel if self.autocommit_enabled else PersonalChannelsModel

    @property
    def model(self):
        return self.channels_stack[-1] if self.channels_stack else None

    def on_channel_committed(self, response):
        if response and response.get("success", False):
            if not self.autocommit_enabled:
                self.commit_control_bar.setHidden(True)
            if self.model:
                self.model.reset()
                self.update_labels()

    def commit_channels(self, checked=False):
        TriblerNetworkRequest("channels/mychannel/0/commit",
                              self.on_channel_committed,
                              method='POST')

    def initialize_content_page(self, autocommit_enabled=False, hide_xxx=None):
        if self.initialized:
            return

        self.hide_xxx = hide_xxx
        self.initialized = True
        self.category_selector.addItems(CATEGORY_SELECTOR_ITEMS)
        connect(self.category_selector.currentIndexChanged,
                self.on_category_selector_changed)
        self.channel_back_button.setIcon(QIcon(
            get_image_path('page_back.png')))
        connect(self.channel_back_button.clicked, self.go_back)
        connect(self.channel_name_label.linkActivated,
                self.on_breadcrumb_clicked)
        self.commit_control_bar.setHidden(True)

        self.controller = ContentTableViewController(
            self.content_table,
            filter_input=self.channel_torrents_filter_input)

        # To reload the preview
        connect(self.channel_preview_button.clicked, self.preview_clicked)

        self.autocommit_enabled = autocommit_enabled
        if self.autocommit_enabled:
            self._enable_autocommit_timer()

        # New channel button
        connect(self.new_channel_button.clicked, self.create_new_channel)
        connect(self.content_table.channel_clicked, self.on_channel_clicked)
        connect(self.edit_channel_commit_button.clicked, self.commit_channels)

        self.subscription_widget.initialize(self)

        self.channel_options_menu = self.create_channel_options_menu()
        self.channel_options_button.setMenu(self.channel_options_menu)

    def _enable_autocommit_timer(self):

        self.commit_timer = QTimer()
        self.commit_timer.setSingleShot(True)
        connect(self.commit_timer.timeout, self.commit_channels)

        # Commit the channel just in case there are uncommitted changes left since the last time (e.g. Tribler crashed)
        # The timer thing here is a workaround for race condition with the core startup
        self.controller.table_view.setColumnHidden(3, True)
        self.commit_timer.stop()
        self.commit_timer.start(10000)

    def on_category_selector_changed(self, ind):
        category = CATEGORY_SELECTOR_ITEMS[ind] if ind else None
        content_category = ContentCategories.get(category)
        category_code = content_category.code if content_category else category
        if self.model.category_filter != category_code:
            self.model.category_filter = category_code
            self.model.reset()

    def empty_channels_stack(self):
        if self.channels_stack:
            self.disconnect_current_model()
            self.channels_stack = []

    def push_channels_stack(self, model):
        if self.model:
            self.model.info_changed.disconnect()
            self.model.saved_header_state = self.controller.table_view.horizontalHeader(
            ).saveState()
            self.model.saved_scroll_state = self.controller.table_view.verticalScrollBar(
            ).value()
            self.controller.unset_model(
            )  # Disconnect the selectionChanged signal
        self.channels_stack.append(model)
        connect(self.model.info_changed, self.on_model_info_changed)

        connect(
            self.window().core_manager.events_manager.
            received_remote_query_results, self.model.on_new_entry_received)
        connect(self.window().core_manager.events_manager.node_info_updated,
                self.model.update_node_info)

        with self.freeze_controls():
            self.category_selector.setCurrentIndex(0)
            self.content_table.horizontalHeader().setSortIndicator(
                -1, Qt.AscendingOrder)
            self.channel_torrents_filter_input.setText("")

    def on_model_info_changed(self, changed_entries):
        self.window().channels_menu_list.reload_if_necessary(changed_entries)
        dirty = False
        structure_changed = False
        for entry in changed_entries:
            dirty = dirty or entry.get('status', None) in DIRTY_STATUSES
            structure_changed = (
                structure_changed or entry.get("state", None) == "Deleted" or
                (entry.get("type", None) in [CHANNEL_TORRENT, COLLECTION_NODE]
                 and entry["status"] in DIRTY_STATUSES))

        if structure_changed:
            self.window().add_to_channel_dialog.clear_channels_tree()

        if self.autocommit_enabled and dirty:
            self.commit_timer.stop()
            self.commit_timer.start(CHANNEL_COMMIT_DELAY)

        # TODO: optimize me: maybe we don't need to update the labels each time?
        self.update_labels(dirty)

    def initialize_root_model_from_channel_info(self, channel_info):
        if channel_info.get("state") == CHANNEL_STATE.PERSONAL.value:
            self.default_channel_model = self.personal_channel_model
        else:
            self.default_channel_model = ChannelContentModel
        model = self.default_channel_model(hide_xxx=self.hide_xxx,
                                           channel_info=channel_info)
        self.initialize_root_model(model)

    def initialize_root_model(self, root_model):
        self.empty_channels_stack()
        self.push_channels_stack(root_model)
        self.controller.set_model(self.model)

    def reset_view(self, text_filter=None):
        self.model.text_filter = text_filter or ''
        self.model.category_filter = None

        with self.freeze_controls():
            self.controller.table_view.horizontalHeader().setSortIndicator(
                -1, Qt.DescendingOrder)
        self.model.sort_by = (
            self.model.columns[self.model.default_sort_column]
            if self.model.default_sort_column >= 0 else None)
        self.model.sort_desc = True
        self.model.reset()

    def disconnect_current_model(self):
        self.model.info_changed.disconnect()
        disconnect(self.window().core_manager.events_manager.node_info_updated,
                   self.model.update_node_info)
        disconnect(
            self.window().core_manager.events_manager.
            received_remote_query_results, self.model.on_new_entry_received)
        self.controller.unset_model()  # Disconnect the selectionChanged signal

    def go_back(self, checked=False):
        if len(self.channels_stack) > 1:
            self.disconnect_current_model()
            self.channels_stack.pop().deleteLater()

            # We block signals to prevent triggering redundant model reloading
            with self.freeze_controls():
                # Set filter category selector to correct index corresponding to loaded model
                content_category = ContentCategories.get(
                    self.model.category_filter)
                filter_display_name = content_category.long_name if content_category else self.model.category_filter
                self.category_selector.setCurrentIndex(
                    CATEGORY_SELECTOR_ITEMS.index(filter_display_name)
                    if filter_display_name in CATEGORY_SELECTOR_ITEMS else 0)
                if self.model.text_filter:
                    self.channel_torrents_filter_input.setText(
                        self.model.text_filter)
                self.controller.set_model(self.model)

            connect(self.model.info_changed, self.on_model_info_changed)
            self.update_labels()

    def on_breadcrumb_clicked(self, tgt_level):
        if int(tgt_level) + 1 != len(self.channels_stack):
            self.go_back_to_level(tgt_level)
        elif isinstance(self.model, SearchResultsModel) and len(
                self.channels_stack) == 1:
            # In case of remote search, when only the search results are on the stack,
            # we must keep the txt_filter (which contains the search term) before resetting the view
            text_filter = self.model.text_filter
            self.reset_view(text_filter=text_filter)
        else:
            # Reset the view if the user clicks on the last part of the breadcrumb
            self.reset_view()

    def go_back_to_level(self, level):
        level = int(level)
        while level + 1 < len(self.channels_stack):
            self.go_back()

    def on_channel_clicked(self, channel_dict):
        self.initialize_with_channel(channel_dict)

    # TODO: restore the method and button to copy channel_id to the clipboard
    # def on_copy_channel_id(self):
    #    copy_to_clipboard(self.channel_info["public_key"])
    #    self.tray_show_message("Copied channel ID", self.channel_info["public_key"])

    def preview_clicked(self, checked=False):
        params = dict()

        if "public_key" in self.model.channel_info:
            # This is a channel contents query, limit the search by channel_pk and origin_id
            params.update({
                'channel_pk': self.model.channel_info["public_key"],
                'origin_id': self.model.channel_info["id"]
            })
        if self.model.text_filter:
            params.update({'txt_filter': self.model.text_filter})
        if self.model.hide_xxx is not None:
            params.update({'hide_xxx': self.model.hide_xxx})
        if self.model.sort_by is not None:
            params.update({'sort_by': self.model.sort_by})
        if self.model.sort_desc is not None:
            params.update({'sort_desc': self.model.sort_desc})
        if self.model.category_filter is not None:
            params.update({'category_filter': self.model.category_filter})

        def add_request_uuid(response):
            request_uuid = response["request_uuid"]
            if self.model:
                self.model.remote_queries.add(uuid.UUID(request_uuid))

        TriblerNetworkRequest('remote_query',
                              add_request_uuid,
                              method="PUT",
                              url_params=params)

    def create_new_channel(self, checked):
        NewChannelDialog(self, self.model.create_new_channel)

    def initialize_with_channel(self, channel_info):
        # Turn off sorting by default to speed up SQL queries
        self.push_channels_stack(
            self.default_channel_model(channel_info=channel_info))
        self.controller.set_model(self.model)
        self.controller.table_view.resizeEvent(None)

        self.content_table.setFocus()

    def update_labels(self, dirty=False):

        folder = self.model.channel_info.get("type", None) == COLLECTION_NODE
        personal = self.model.channel_info.get("state", None) == "Personal"
        root = len(self.channels_stack) == 1
        legacy = self.model.channel_info.get("state", None) == "Legacy"
        complete = self.model.channel_info.get("state", None) == "Complete"
        search = isinstance(self.model, SearchResultsModel)
        discovered = isinstance(self.model, DiscoveredChannelsModel)
        personal_model = isinstance(self.model, PersonalChannelsModel)
        is_a_channel = self.model.channel_info.get("type",
                                                   None) == CHANNEL_TORRENT

        self.category_selector.setHidden(root
                                         and (discovered or personal_model))
        # initialize the channel page

        # Assemble the channels navigation breadcrumb by utilising RichText links feature
        self.channel_name_label.setTextFormat(Qt.RichText)
        # We build the breadcrumb text backwards, by performing lookahead on each step.
        # While building the breadcrumb label in RichText we also assemble an undecorated variant of the same text
        # to estimate if we need to elide the breadcrumb. We cannot use RichText contents directly with
        # .elidedText method because QT will elide the tags as well.
        breadcrumb_text = ''
        breadcrumb_text_undecorated = ''
        path_parts = [(m, model.channel_info["name"])
                      for m, model in enumerate(self.channels_stack)]
        slash_separator = '<font color=#CCCCCC>  /  </font>'
        for m, channel_name in reversed(path_parts):
            breadcrumb_text_undecorated = " / " + channel_name + breadcrumb_text_undecorated
            breadcrumb_text_elided = self.channel_name_label.fontMetrics(
            ).elidedText(breadcrumb_text_undecorated, 0,
                         self.channel_name_label.width())
            must_elide = breadcrumb_text_undecorated != breadcrumb_text_elided
            if must_elide:
                channel_name = "..."
            breadcrumb_text = (
                slash_separator +
                f'<a style="text-decoration:none;color:#A5A5A5;" href="{m}">{channel_name}</a>'
                + breadcrumb_text)
            if must_elide:
                break
        # Remove the leftmost slash:
        if len(breadcrumb_text) >= len(slash_separator):
            breadcrumb_text = breadcrumb_text[len(slash_separator):]

        self.edit_channel_contents_top_bar.setHidden(not personal)
        self.new_channel_button.setText(
            "NEW CHANNEL" if not is_a_channel and not folder else "NEW FOLDER")

        self.channel_name_label.setText(breadcrumb_text)
        self.channel_name_label.setTextInteractionFlags(
            Qt.TextBrowserInteraction)

        self.channel_back_button.setHidden(root)
        self.channel_options_button.setHidden(not personal_model
                                              or not personal
                                              or (root and not is_a_channel))
        self.new_channel_button.setHidden(not personal_model or not personal)

        self.channel_state_label.setText(
            self.model.channel_info.get("state",
                                        "This text should not ever be shown"))

        self.subscription_widget.setHidden(not is_a_channel or personal
                                           or folder or legacy)
        if not self.subscription_widget.isHidden():
            self.subscription_widget.update_subscribe_button(
                self.model.channel_info)

        self.channel_preview_button.setHidden(
            (root and not search and not is_a_channel) or personal or legacy
            or complete)
        self.channel_state_label.setHidden((root and not is_a_channel)
                                           or personal)

        self.commit_control_bar.setHidden(self.autocommit_enabled or not dirty
                                          or not personal)

        if "total" in self.model.channel_info:
            if "torrents" in self.model.channel_info:
                self.channel_num_torrents_label.setText(
                    f"{self.model.channel_info['total']}/{self.model.channel_info['torrents']} items"
                )
            else:
                self.channel_num_torrents_label.setText(
                    f"{self.model.channel_info['total']} items")

    # ==============================
    # Channel menu related methods.
    # TODO: make this into a separate object, stop reconnecting stuff each time
    # ==============================

    def create_channel_options_menu(self):
        browse_files_action = QAction('Add .torrent file', self)
        browse_dir_action = QAction('Add torrent(s) directory', self)
        add_url_action = QAction('Add URL/magnet links', self)

        connect(browse_files_action.triggered, self.on_add_torrent_browse_file)
        connect(browse_dir_action.triggered, self.on_add_torrents_browse_dir)
        connect(add_url_action.triggered, self.on_add_torrent_from_url)

        channel_options_menu = TriblerActionMenu(self)
        channel_options_menu.addAction(browse_files_action)
        channel_options_menu.addAction(browse_dir_action)
        channel_options_menu.addAction(add_url_action)
        return channel_options_menu

    # Torrent addition-related methods
    def on_add_torrents_browse_dir(self, checked):
        chosen_dir = QFileDialog.getExistingDirectory(
            self, "Please select the directory containing the .torrent files",
            QDir.homePath(), QFileDialog.ShowDirsOnly)
        if not chosen_dir:
            return

        self.chosen_dir = chosen_dir
        self.dialog = ConfirmationDialog(
            self,
            "Add torrents from directory",
            f"Add all torrent files from the following directory to your Tribler channel:\n\n{chosen_dir}",
            [('ADD', BUTTON_TYPE_NORMAL), ('CANCEL', BUTTON_TYPE_CONFIRM)],
            checkbox_text="Include subdirectories (recursive mode)",
        )
        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:
            self.add_dir_to_channel(self.chosen_dir,
                                    recursive=self.dialog.checkbox.isChecked())

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

    def on_add_torrent_browse_file(self, checked):
        filenames = QFileDialog.getOpenFileNames(
            self, "Please select the .torrent file", "",
            "Torrent files (*.torrent)")
        if not filenames[0]:
            return

        for filename in filenames[0]:
            self.add_torrent_to_channel(filename)

    def on_add_torrent_from_url(self, checked):
        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')
        connect(self.dialog.button_clicked,
                self.on_torrent_from_url_dialog_done)
        self.dialog.show()

    def on_torrent_from_url_dialog_done(self, action):
        if action == 0:
            self.add_torrent_url_to_channel(
                self.dialog.dialog_widget.dialog_input.text())
        self.dialog.close_dialog()
        self.dialog = None

    def _on_torrent_to_channel_added(self, result):
        # TODO: just add it at the top of the list instead
        if not result:
            return
        if result.get('added'):
            # FIXME: dumb hack to adapt torrents PUT endpoint output to the info_changed signal
            # If thousands of torrents were added, we don't want to post them all in a single
            # REST response. Instead, we always provide the total number of new torrents.
            # If we add a single torrent though, the endpoint will return it as a dict.
            # However, on_model_info_changed always expects a list of changed entries.
            # So, we make up the list.
            results_list = result['added']
            if isinstance(results_list, dict):
                results_list = [results_list]
            elif isinstance(results_list, int):
                results_list = [{'status': NEW}]
            self.model.info_changed.emit(results_list)
            self.model.reset()

    def _add_torrent_request(self, data):
        TriblerNetworkRequest(
            f'collections/mychannel/{self.model.channel_info["id"]}/torrents',
            self._on_torrent_to_channel_added,
            method='PUT',
            data=data,
        )

    def add_torrent_to_channel(self, filename):
        with open(filename, "rb") as torrent_file:
            torrent_content = b64encode(torrent_file.read()).decode('utf-8')
        self._add_torrent_request({"torrent": torrent_content})

    def add_dir_to_channel(self, dirname, recursive=False):
        self._add_torrent_request({
            "torrents_dir": dirname,
            "recursive": int(recursive)
        })

    def add_torrent_url_to_channel(self, url):
        self._add_torrent_request({"uri": url})
Esempio n. 10
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)