From 69006b5872e781a381ac1cddf6aeb1c8b8f102d5 Mon Sep 17 00:00:00 2001 From: bluegreenmagick Date: Wed, 3 Mar 2021 10:34:43 +0900 Subject: [PATCH 1/6] add dialog to choose addons to update --- ftl/qt/addons.ftl | 1 + qt/aqt/addons.py | 147 +++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 133 insertions(+), 15 deletions(-) diff --git a/ftl/qt/addons.ftl b/ftl/qt/addons.ftl index 43894125d..07ed57887 100644 --- a/ftl/qt/addons.ftl +++ b/ftl/qt/addons.ftl @@ -64,3 +64,4 @@ addons-delete-the-numd-selected-addon = [one] Delete the { $count } selected add-on? *[other] Delete the { $count } selected add-ons? } +addons-choose-update-window-title = Update Add-ons diff --git a/qt/aqt/addons.py b/qt/aqt/addons.py index 0166276c9..9957780de 100644 --- a/qt/aqt/addons.py +++ b/qt/aqt/addons.py @@ -10,6 +10,7 @@ import zipfile from collections import defaultdict from concurrent.futures import Future from dataclasses import dataclass +from datetime import datetime from typing import IO, Any, Callable, Dict, Iterable, List, Optional, Tuple, Union from urllib.parse import parse_qs, urlparse from zipfile import ZipFile @@ -551,19 +552,19 @@ class AddonManager: if updated: self.write_addon_meta(addon) - def updates_required(self, items: List[UpdateInfo]) -> List[int]: + def updates_required(self, items: List[UpdateInfo]) -> List[UpdateInfo]: """Return ids of add-ons requiring an update.""" need_update = [] for item in items: addon = self.addon_meta(str(item.id)) # update if server mtime is newer if not addon.is_latest(item.suitable_branch_last_modified): - need_update.append(item.id) + need_update.append(item) elif not addon.compatible() and item.suitable_branch_last_modified > 0: # Addon is currently disabled, and a suitable branch was found on the # server. Ignore our stored mtime (which may have been set incorrectly # in the past) and require an update. - need_update.append(item.id) + need_update.append(item) return need_update @@ -1132,6 +1133,128 @@ def download_addons( ###################################################################### +class ChooseAddonsToUpdateList(QListWidget): + ADDON_ID_ROLE = 101 + + def __init__( + self, + parent: QWidget, + mgr: AddonManager, + updated_addons: List[UpdateInfo], + ) -> None: + QListWidget.__init__(self, parent) + self.mgr = mgr + self.updated_addons = sorted( + updated_addons, key=lambda addon: addon.suitable_branch_last_modified + ) + self.setup() + qconnect(self.itemClicked, self.toggle_check) + qconnect(self.itemDoubleClicked, self.double_click) + + def setup(self) -> None: + check_state = Qt.Unchecked + header_item = QListWidgetItem("", self) + header_item.setFlags(Qt.ItemFlag(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled)) + header_item.setBackground(Qt.lightGray) + header_item.setCheckState(check_state) + self.header_item = header_item + for update_info in self.updated_addons: + addon_id = update_info.id + addon_name = self.mgr.addon_meta(str(addon_id)).human_name() + update_timestamp = update_info.suitable_branch_last_modified + update_time = datetime.fromtimestamp(update_timestamp) + + addon_label = f"{update_time:%Y-%m-%d} {addon_name}" + item = QListWidgetItem(addon_label, self) + # Not user checkable because it overlaps with itemClicked signal + item.setFlags(Qt.ItemFlag(Qt.ItemIsEnabled)) + item.setCheckState(check_state) + item.setData(self.ADDON_ID_ROLE, addon_id) + + def toggle_check(self, item: QListWidgetItem) -> None: + if item == self.header_item: + if item.checkState() == Qt.Checked: + check = Qt.Checked + else: + check = Qt.Unchecked + self.check_all_items(check) + return + # Normal Item + if item.checkState() == Qt.Checked: + item.setCheckState(Qt.Unchecked) + self.header_item.setCheckState(Qt.Unchecked) + else: + item.setCheckState(Qt.Checked) + if self.every_item_is_checked(): + self.header_item.setCheckState(Qt.Checked) + + def double_click(self, item: QListWidgetItem) -> None: + if item == self.header_item: + if item.checkState() == Qt.Checked: + check = Qt.Unchecked + else: + check = Qt.Checked + self.header_item.setCheckState(check) + self.check_all_items(check) + + def check_all_items(self, check: Qt.CheckState = Qt.Checked) -> None: + for i in range(1, self.count()): + self.item(i).setCheckState(check) + + def every_item_is_checked(self) -> bool: + for i in range(1, self.count()): + item = self.item(i) + if item.checkState() == Qt.Unchecked: + return False + return True + + def get_selected_addon_ids(self) -> List[int]: + addon_ids = [] + for i in range(1, self.count()): + item = self.item(i) + if item.checkState() == Qt.Checked: + addon_ids.append(item.data(self.ADDON_ID_ROLE)) + return addon_ids + + +class ChooseAddonsToUpdateDialog(QDialog): + def __init__( + self, parent: QWidget, mgr: AddonManager, updated_addons: List[UpdateInfo] + ) -> None: + QDialog.__init__(self, parent) + self.setWindowTitle(tr(TR.ADDONS_CHOOSE_UPDATE_WINDOW_TITLE)) + self.setWindowModality(Qt.WindowModal) + self.mgr = mgr + self.updated_addons = updated_addons + self.setup() + restoreGeom(self, "addonsChooseUpdate") + + def setup(self) -> None: + layout = QVBoxLayout() + label = QLabel(tr(TR.ADDONS_THE_FOLLOWING_ADDONS_HAVE_UPDATES_AVAILABLE)) + layout.addWidget(label) + addons_list_widget = ChooseAddonsToUpdateList( + self, self.mgr, self.updated_addons + ) + layout.addWidget(addons_list_widget) + self.addons_list_widget = addons_list_widget + + button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) # type: ignore + qconnect(button_box.button(QDialogButtonBox.Ok).clicked, self.accept) + qconnect(button_box.button(QDialogButtonBox.Cancel).clicked, self.reject) + layout.addWidget(button_box) + self.setLayout(layout) + + def ask(self) -> List[int]: + "Returns a list of selected addons' ids" + ret = self.exec_() + saveGeom(self, "addonsChooseUpdate") + if ret == QDialog.Accepted: + return self.addons_list_widget.get_selected_addon_ids() + else: + return [] + + def fetch_update_info(client: HttpClient, ids: List[int]) -> List[Dict]: """Fetch update info from AnkiWeb in one or more batches.""" all_info: List[Dict] = [] @@ -1239,31 +1362,25 @@ def handle_update_info( update_info = mgr.extract_update_info(items) mgr.update_supported_versions(update_info) - updated_ids = mgr.updates_required(update_info) + updated_addons = mgr.updates_required(update_info) - if not updated_ids: + if not updated_addons: on_done([]) return - prompt_to_update(parent, mgr, client, updated_ids, on_done) + prompt_to_update(parent, mgr, client, updated_addons, on_done) def prompt_to_update( parent: QWidget, mgr: AddonManager, client: HttpClient, - ids: List[int], + updated_addons: List[UpdateInfo], on_done: Callable[[List[DownloadLogEntry]], None], ) -> None: - names = map(lambda x: mgr.addonName(str(x)), ids) - if not askUser( - tr(TR.ADDONS_THE_FOLLOWING_ADDONS_HAVE_UPDATES_AVAILABLE) - + "\n\n" - + "\n".join(names) - ): - # on_done is not called if the user cancels + ids = ChooseAddonsToUpdateDialog(parent, mgr, updated_addons).ask() + if not ids: return - download_addons(parent, mgr, ids, on_done, client) From e73e0bec34ca64c74fd5596eef208824420c42cf Mon Sep 17 00:00:00 2001 From: bluegreenmagick Date: Wed, 3 Mar 2021 11:23:59 +0900 Subject: [PATCH 2/6] save chooseaddonupdate check state --- qt/aqt/addons.py | 40 +++++++++++++++++++++++++++++----------- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/qt/aqt/addons.py b/qt/aqt/addons.py index 9957780de..1ecc3e425 100644 --- a/qt/aqt/addons.py +++ b/qt/aqt/addons.py @@ -105,6 +105,7 @@ class AddonMeta: max_point_version: int branch_index: int human_version: Optional[str] + update_enabled: bool def human_name(self) -> str: return self.provided_name or self.dir_name @@ -140,6 +141,7 @@ class AddonMeta: max_point_version=json_meta.get("max_point_version", 0) or 0, branch_index=json_meta.get("branch_index", 0) or 0, human_version=json_meta.get("human_version"), + update_enabled=json_meta.get("update_enabled", True), ) @@ -243,6 +245,7 @@ class AddonManager: json_obj["branch_index"] = addon.branch_index if addon.human_version is not None: json_obj["human_version"] = addon.human_version + json_obj["update_enabled"] = addon.update_enabled self.writeAddonMeta(addon.dir_name, json_obj) @@ -1152,15 +1155,15 @@ class ChooseAddonsToUpdateList(QListWidget): qconnect(self.itemDoubleClicked, self.double_click) def setup(self) -> None: - check_state = Qt.Unchecked header_item = QListWidgetItem("", self) header_item.setFlags(Qt.ItemFlag(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled)) header_item.setBackground(Qt.lightGray) - header_item.setCheckState(check_state) self.header_item = header_item for update_info in self.updated_addons: addon_id = update_info.id - addon_name = self.mgr.addon_meta(str(addon_id)).human_name() + addon_meta = self.mgr.addon_meta(str(addon_id)) + update_enabled = addon_meta.update_enabled + addon_name = addon_meta.human_name() update_timestamp = update_info.suitable_branch_last_modified update_time = datetime.fromtimestamp(update_timestamp) @@ -1168,8 +1171,12 @@ class ChooseAddonsToUpdateList(QListWidget): item = QListWidgetItem(addon_label, self) # Not user checkable because it overlaps with itemClicked signal item.setFlags(Qt.ItemFlag(Qt.ItemIsEnabled)) - item.setCheckState(check_state) + if update_enabled: + item.setCheckState(Qt.Checked) + else: + item.setCheckState(Qt.Unchecked) item.setData(self.ADDON_ID_ROLE, addon_id) + self.refresh_header_check_state() def toggle_check(self, item: QListWidgetItem) -> None: if item == self.header_item: @@ -1182,11 +1189,9 @@ class ChooseAddonsToUpdateList(QListWidget): # Normal Item if item.checkState() == Qt.Checked: item.setCheckState(Qt.Unchecked) - self.header_item.setCheckState(Qt.Unchecked) else: item.setCheckState(Qt.Checked) - if self.every_item_is_checked(): - self.header_item.setCheckState(Qt.Checked) + self.refresh_header_check_state() def double_click(self, item: QListWidgetItem) -> None: if item == self.header_item: @@ -1201,21 +1206,33 @@ class ChooseAddonsToUpdateList(QListWidget): for i in range(1, self.count()): self.item(i).setCheckState(check) - def every_item_is_checked(self) -> bool: + def refresh_header_check_state(self) -> None: for i in range(1, self.count()): item = self.item(i) if item.checkState() == Qt.Unchecked: - return False - return True + self.header_item.setCheckState(Qt.Unchecked) + return + self.header_item.setCheckState(Qt.Checked) def get_selected_addon_ids(self) -> List[int]: addon_ids = [] for i in range(1, self.count()): item = self.item(i) if item.checkState() == Qt.Checked: - addon_ids.append(item.data(self.ADDON_ID_ROLE)) + addon_id = item.data(self.ADDON_ID_ROLE) + addon_ids.append(addon_id) return addon_ids + def save_check_state(self) -> None: + for i in range(1, self.count()): + item = self.item(i) + addon_id = item.data(self.ADDON_ID_ROLE) + addon_meta = self.mgr.addon_meta(str(addon_id)) + checked = item.checkState() == Qt.Checked + print(checked) + addon_meta.update_enabled = checked + self.mgr.write_addon_meta(addon_meta) + class ChooseAddonsToUpdateDialog(QDialog): def __init__( @@ -1249,6 +1266,7 @@ class ChooseAddonsToUpdateDialog(QDialog): "Returns a list of selected addons' ids" ret = self.exec_() saveGeom(self, "addonsChooseUpdate") + self.addons_list_widget.save_check_state() if ret == QDialog.Accepted: return self.addons_list_widget.get_selected_addon_ids() else: From 01d8dc20f1576da377d470d1cb12292d9f4363df Mon Sep 17 00:00:00 2001 From: bluegreenmagick Date: Wed, 3 Mar 2021 11:54:37 +0900 Subject: [PATCH 3/6] fix check issues in ChooseAddonsToUpdateList --- qt/aqt/addons.py | 49 +++++++++++++++++++++++++++++------------------- 1 file changed, 30 insertions(+), 19 deletions(-) diff --git a/qt/aqt/addons.py b/qt/aqt/addons.py index 1ecc3e425..246087f97 100644 --- a/qt/aqt/addons.py +++ b/qt/aqt/addons.py @@ -1150,9 +1150,11 @@ class ChooseAddonsToUpdateList(QListWidget): self.updated_addons = sorted( updated_addons, key=lambda addon: addon.suitable_branch_last_modified ) + self.ignore_check_evt = False self.setup() - qconnect(self.itemClicked, self.toggle_check) - qconnect(self.itemDoubleClicked, self.double_click) + qconnect(self.itemClicked, self.on_click) + qconnect(self.itemChanged, self.on_check) + qconnect(self.itemDoubleClicked, self.on_double_click) def setup(self) -> None: header_item = QListWidgetItem("", self) @@ -1178,41 +1180,51 @@ class ChooseAddonsToUpdateList(QListWidget): item.setData(self.ADDON_ID_ROLE, addon_id) self.refresh_header_check_state() - def toggle_check(self, item: QListWidgetItem) -> None: + def on_click(self, item: QListWidgetItem) -> None: if item == self.header_item: - if item.checkState() == Qt.Checked: - check = Qt.Checked - else: - check = Qt.Unchecked - self.check_all_items(check) return - # Normal Item if item.checkState() == Qt.Checked: - item.setCheckState(Qt.Unchecked) + self.check_item(item, Qt.Unchecked) else: - item.setCheckState(Qt.Checked) + self.check_item(item, Qt.Checked) self.refresh_header_check_state() - def double_click(self, item: QListWidgetItem) -> None: + def on_check(self, item: QListWidgetItem) -> None: + if self.ignore_check_evt: + return + if item == self.header_item: + if item.checkState() == Qt.Checked: + check = Qt.Checked + else: + check = Qt.Unchecked + self.header_checked(check) + + def on_double_click(self, item: QListWidgetItem) -> None: if item == self.header_item: if item.checkState() == Qt.Checked: check = Qt.Unchecked else: check = Qt.Checked - self.header_item.setCheckState(check) - self.check_all_items(check) + self.check_item(self.header_item, check) + self.header_checked(check) - def check_all_items(self, check: Qt.CheckState = Qt.Checked) -> None: + def check_item(self, item: QListWidgetItem, check: Qt.CheckState) -> None: + "call item.setCheckState without triggering on_check" + self.ignore_check_evt = True + item.setCheckState(check) + self.ignore_check_evt = False + + def header_checked(self, check: Qt.CheckState) -> None: for i in range(1, self.count()): - self.item(i).setCheckState(check) + self.check_item(self.item(i), check) def refresh_header_check_state(self) -> None: for i in range(1, self.count()): item = self.item(i) if item.checkState() == Qt.Unchecked: - self.header_item.setCheckState(Qt.Unchecked) + self.check_item(self.header_item, Qt.Unchecked) return - self.header_item.setCheckState(Qt.Checked) + self.check_item(self.header_item, Qt.Checked) def get_selected_addon_ids(self) -> List[int]: addon_ids = [] @@ -1229,7 +1241,6 @@ class ChooseAddonsToUpdateList(QListWidget): addon_id = item.data(self.ADDON_ID_ROLE) addon_meta = self.mgr.addon_meta(str(addon_id)) checked = item.checkState() == Qt.Checked - print(checked) addon_meta.update_enabled = checked self.mgr.write_addon_meta(addon_meta) From c11feda0eb4e16c276cb562c983d0a475fed2a9b Mon Sep 17 00:00:00 2001 From: bluegreenmagick Date: Wed, 3 Mar 2021 12:05:24 +0900 Subject: [PATCH 4/6] add bool_to_check and checked method --- qt/aqt/addons.py | 37 ++++++++++++++++++------------------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/qt/aqt/addons.py b/qt/aqt/addons.py index 246087f97..9fba37947 100644 --- a/qt/aqt/addons.py +++ b/qt/aqt/addons.py @@ -1180,33 +1180,33 @@ class ChooseAddonsToUpdateList(QListWidget): item.setData(self.ADDON_ID_ROLE, addon_id) self.refresh_header_check_state() + def bool_to_check(self, check_bool: bool) -> Qt.CheckState: + if check_bool: + return Qt.Checked + else: + return Qt.Unchecked + + def checked(self, item: QListWidgetItem) -> bool: + return item.checkState() == Qt.Checked + def on_click(self, item: QListWidgetItem) -> None: if item == self.header_item: return - if item.checkState() == Qt.Checked: - self.check_item(item, Qt.Unchecked) - else: - self.check_item(item, Qt.Checked) + checked = self.checked(item) + self.check_item(item, self.bool_to_check(not checked)) self.refresh_header_check_state() def on_check(self, item: QListWidgetItem) -> None: if self.ignore_check_evt: return if item == self.header_item: - if item.checkState() == Qt.Checked: - check = Qt.Checked - else: - check = Qt.Unchecked - self.header_checked(check) + self.header_checked(item.checkState()) def on_double_click(self, item: QListWidgetItem) -> None: if item == self.header_item: - if item.checkState() == Qt.Checked: - check = Qt.Unchecked - else: - check = Qt.Checked - self.check_item(self.header_item, check) - self.header_checked(check) + checked = self.checked(item) + self.check_item(self.header_item, self.bool_to_check(not checked)) + self.header_checked(self.bool_to_check(not checked)) def check_item(self, item: QListWidgetItem, check: Qt.CheckState) -> None: "call item.setCheckState without triggering on_check" @@ -1221,7 +1221,7 @@ class ChooseAddonsToUpdateList(QListWidget): def refresh_header_check_state(self) -> None: for i in range(1, self.count()): item = self.item(i) - if item.checkState() == Qt.Unchecked: + if not self.checked(item): self.check_item(self.header_item, Qt.Unchecked) return self.check_item(self.header_item, Qt.Checked) @@ -1230,7 +1230,7 @@ class ChooseAddonsToUpdateList(QListWidget): addon_ids = [] for i in range(1, self.count()): item = self.item(i) - if item.checkState() == Qt.Checked: + if self.checked(item): addon_id = item.data(self.ADDON_ID_ROLE) addon_ids.append(addon_id) return addon_ids @@ -1240,8 +1240,7 @@ class ChooseAddonsToUpdateList(QListWidget): item = self.item(i) addon_id = item.data(self.ADDON_ID_ROLE) addon_meta = self.mgr.addon_meta(str(addon_id)) - checked = item.checkState() == Qt.Checked - addon_meta.update_enabled = checked + addon_meta.update_enabled = self.checked(item) self.mgr.write_addon_meta(addon_meta) From 29076ec9ec5e62b60cd4dc032c369e6018dfa570 Mon Sep 17 00:00:00 2001 From: bluegreenmagick Date: Wed, 3 Mar 2021 12:17:56 +0900 Subject: [PATCH 5/6] add context menu to open ankiweb page --- qt/aqt/addons.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/qt/aqt/addons.py b/qt/aqt/addons.py index 9fba37947..0459df6eb 100644 --- a/qt/aqt/addons.py +++ b/qt/aqt/addons.py @@ -1152,9 +1152,11 @@ class ChooseAddonsToUpdateList(QListWidget): ) self.ignore_check_evt = False self.setup() + self.setContextMenuPolicy(Qt.CustomContextMenu) qconnect(self.itemClicked, self.on_click) qconnect(self.itemChanged, self.on_check) qconnect(self.itemDoubleClicked, self.on_double_click) + qconnect(self.customContextMenuRequested, self.on_context_menu) def setup(self) -> None: header_item = QListWidgetItem("", self) @@ -1208,6 +1210,14 @@ class ChooseAddonsToUpdateList(QListWidget): self.check_item(self.header_item, self.bool_to_check(not checked)) self.header_checked(self.bool_to_check(not checked)) + def on_context_menu(self, point: QPoint) -> None: + item = self.itemAt(point) + addon_id = item.data(self.ADDON_ID_ROLE) + m = QMenu() + a = m.addAction(tr(TR.ADDONS_VIEW_ADDON_PAGE)) + qconnect(a.triggered, lambda _: openLink(f"{aqt.appShared}info/{addon_id}")) + m.exec_(QCursor.pos()) + def check_item(self, item: QListWidgetItem, check: Qt.CheckState) -> None: "call item.setCheckState without triggering on_check" self.ignore_check_evt = True From 4cde93ed747db2badcf60827adfca625e0d3a05d Mon Sep 17 00:00:00 2001 From: bluegreenmagick Date: Tue, 9 Mar 2021 22:27:28 +0900 Subject: [PATCH 6/6] don't show routine update when not update_enabled --- qt/aqt/addons.py | 15 +++++++++++++-- qt/aqt/main.py | 5 ++++- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/qt/aqt/addons.py b/qt/aqt/addons.py index 0459df6eb..e50ac35db 100644 --- a/qt/aqt/addons.py +++ b/qt/aqt/addons.py @@ -1325,9 +1325,10 @@ def check_and_prompt_for_updates( parent: QWidget, mgr: AddonManager, on_done: Callable[[List[DownloadLogEntry]], None], + requested_by_user: bool = True, ) -> None: def on_updates_received(client: HttpClient, items: List[Dict]) -> None: - handle_update_info(parent, mgr, client, items, on_done) + handle_update_info(parent, mgr, client, items, on_done, requested_by_user) check_for_updates(mgr, on_updates_received) @@ -1396,6 +1397,7 @@ def handle_update_info( client: HttpClient, items: List[Dict], on_done: Callable[[List[DownloadLogEntry]], None], + requested_by_user: bool = True, ) -> None: update_info = mgr.extract_update_info(items) mgr.update_supported_versions(update_info) @@ -1406,7 +1408,7 @@ def handle_update_info( on_done([]) return - prompt_to_update(parent, mgr, client, updated_addons, on_done) + prompt_to_update(parent, mgr, client, updated_addons, on_done, requested_by_user) def prompt_to_update( @@ -1415,7 +1417,16 @@ def prompt_to_update( client: HttpClient, updated_addons: List[UpdateInfo], on_done: Callable[[List[DownloadLogEntry]], None], + requested_by_user: bool = True, ) -> None: + if not requested_by_user: + prompt_update = False + for addon in updated_addons: + if mgr.addon_meta(str(addon.id)).update_enabled: + prompt_update = True + if not prompt_update: + return + ids = ChooseAddonsToUpdateDialog(parent, mgr, updated_addons).ask() if not ids: return diff --git a/qt/aqt/main.py b/qt/aqt/main.py index 00217ff82..bf3b5d49d 100644 --- a/qt/aqt/main.py +++ b/qt/aqt/main.py @@ -848,7 +848,10 @@ title="%s" %s>%s""" % ( if elap > 86_400: check_and_prompt_for_updates( - self, self.addonManager, self.on_updates_installed + self, + self.addonManager, + self.on_updates_installed, + requested_by_user=False, ) self.pm.set_last_addon_update_check(intTime())