From 9684457744a0bd731981f6289f776e15bcbf8d8c Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 5 Mar 2025 15:47:07 +0530 Subject: [PATCH] More work on the connect to folder dialog --- src/calibre/customize/ui.py | 2 +- src/calibre/devices/eb600/driver.py | 11 +- src/calibre/devices/hanvon/driver.py | 2 +- src/calibre/devices/interface.py | 4 + src/calibre/devices/kindle/driver.py | 6 +- src/calibre/devices/misc.py | 4 +- src/calibre/devices/nook/driver.py | 3 +- src/calibre/devices/prs505/driver.py | 2 +- src/calibre/devices/prst1/driver.py | 2 +- src/calibre/devices/usbms/device.py | 5 +- src/calibre/gui2/dialogs/connect_to_folder.py | 123 +++++++++++++++++- 11 files changed, 147 insertions(+), 17 deletions(-) diff --git a/src/calibre/customize/ui.py b/src/calibre/customize/ui.py index 501ac6a1e2..8e229a6620 100644 --- a/src/calibre/customize/ui.py +++ b/src/calibre/customize/ui.py @@ -674,7 +674,7 @@ def device_plugins(include_disabled=False): def usbms_plugins(include_disabled=True): from calibre.devices.usbms.driver import USBMS for plugin in device_plugins(include_disabled): - if isinstance(plugin, USBMS) and plugin.name not in ('Folder Device Interface',): + if isinstance(plugin, USBMS) and plugin.name not in ('Folder Device Interface', 'User Defined USB driver'): yield plugin diff --git a/src/calibre/devices/eb600/driver.py b/src/calibre/devices/eb600/driver.py index b27201b352..3737a9a3f0 100644 --- a/src/calibre/devices/eb600/driver.py +++ b/src/calibre/devices/eb600/driver.py @@ -293,7 +293,7 @@ class INVESBOOK(EB600): class BOOQ(EB600): name = 'Booq Device Interface' - gui_name = 'bq Reader' + gui_name = 'Bq Reader' FORMATS = ['epub', 'mobi', 'prc', 'fb2', 'pdf', 'doc', 'rtf', 'txt', 'html'] @@ -330,6 +330,7 @@ class ELONEX(EB600): class POCKETBOOK301(USBMS): name = 'PocketBook 301 Device Interface' + gui_name = 'PocketBook 301' description = _('Communicate with the PocketBook 301 Reader.') author = 'Kovid Goyal' supported_platforms = ['windows', 'osx', 'linux'] @@ -348,7 +349,7 @@ class POCKETBOOK301(USBMS): class POCKETBOOK602(USBMS): name = 'PocketBook Pro 602/902 Device Interface' - gui_name = 'PocketBook' + gui_name = 'PocketBook Pro' description = _('Communicate with the PocketBook 515/602/603/902/903/Pro 912 reader.') author = 'Kovid Goyal' supported_platforms = ['windows', 'osx', 'linux'] @@ -371,6 +372,7 @@ class POCKETBOOK602(USBMS): class POCKETBOOK622(POCKETBOOK602): name = 'PocketBook 622 Device Interface' + gui_name = 'PocketBook 622' description = _('Communicate with the PocketBook 622 and 623 readers.') EBOOK_DIR_MAIN = '' @@ -385,6 +387,7 @@ class POCKETBOOK622(POCKETBOOK602): class POCKETBOOK360P(POCKETBOOK602): name = 'PocketBook 360+ Device Interface' + gui_name = 'PocketBook 360+' description = _('Communicate with the PocketBook 360+ reader.') BCD = [0x0323] EBOOK_DIR_MAIN = '' @@ -396,7 +399,7 @@ class POCKETBOOK360P(POCKETBOOK602): class POCKETBOOK701(USBMS): name = 'PocketBook 701 Device Interface' - gui_name = 'PocketBook' + gui_name = 'PocketBook 701' description = _('Communicate with the PocketBook 701') author = _('Kovid Goyal') @@ -428,7 +431,7 @@ class POCKETBOOK701(USBMS): class POCKETBOOK740(USBMS): name = 'PocketBook 740 Device Interface' - gui_name = 'PocketBook' + gui_name = 'PocketBook 740' description = _('Communicate with the PocketBook 740') supported_platforms = ['windows', 'osx', 'linux'] FORMATS = ['epub', 'fb2', 'prc', 'mobi', 'pdf', 'djvu', 'rtf', 'chm', diff --git a/src/calibre/devices/hanvon/driver.py b/src/calibre/devices/hanvon/driver.py index e55b3fe80d..83cdd122b2 100644 --- a/src/calibre/devices/hanvon/driver.py +++ b/src/calibre/devices/hanvon/driver.py @@ -61,7 +61,7 @@ class KIBANO(N516): class THEBOOK(N516): name = 'The Book driver' - gui_name = 'The Book' + gui_name = 'Book' description = _('Communicate with The Book reader.') author = 'Kovid Goyal' diff --git a/src/calibre/devices/interface.py b/src/calibre/devices/interface.py index 2c3f16d7a1..93e9ae830c 100644 --- a/src/calibre/devices/interface.py +++ b/src/calibre/devices/interface.py @@ -17,6 +17,10 @@ class ModelMetadata(NamedTuple): bcd: int driver_class: type + @property + def settings_key(self) -> str: + return f'{self.manufacturer_name} - {self.model_name}' + class OpenPopupMessage: diff --git a/src/calibre/devices/kindle/driver.py b/src/calibre/devices/kindle/driver.py index 904c459b06..b7f8723d56 100644 --- a/src/calibre/devices/kindle/driver.py +++ b/src/calibre/devices/kindle/driver.py @@ -82,7 +82,7 @@ def get_files_in(path): class KINDLE(USBMS): name = 'Kindle Device Interface' - gui_name = 'Amazon Kindle' + gui_name = 'Amazon Kindle Keyboard' icon = 'devices/kindle.png' description = _('Communicate with the Kindle e-book reader.') author = 'John Schember' @@ -369,6 +369,7 @@ class KINDLE(USBMS): class KINDLE2(KINDLE): name = 'Kindle 2/3/4/Touch/PaperWhite/Voyage Device Interface' + gui_name = 'Amazon Kindle' description = _('Communicate with the Kindle 2/3/4/Touch/Paperwhite/Voyage e-book reader.') FORMATS = ['azw', 'mobi', 'azw3', 'prc', 'azw1', 'tpz', 'azw4', 'kfx', 'pobi', 'pdf', 'txt'] @@ -649,6 +650,7 @@ class KINDLE2(KINDLE): class KINDLE_DX(KINDLE2): name = 'Kindle DX Device Interface' + gui_name = 'Amazon Kindle DX' description = _('Communicate with the Kindle DX e-book reader.') FORMATS = ['azw', 'mobi', 'prc', 'azw1', 'tpz', 'azw4', 'pobi', 'pdf', 'txt'] @@ -666,7 +668,7 @@ class KINDLE_FIRE(KINDLE2): name = 'Kindle Fire Device Interface' description = _('Communicate with the Kindle Fire') - gui_name = 'Fire' + gui_name = 'Amazon Kindle Fire' FORMATS = ['azw3', 'azw', 'mobi', 'prc', 'azw1', 'tpz', 'azw4', 'kfx', 'pobi', 'pdf', 'txt'] PRODUCT_ID = [0x0006] diff --git a/src/calibre/devices/misc.py b/src/calibre/devices/misc.py index 536b9e6bea..4357b3e217 100644 --- a/src/calibre/devices/misc.py +++ b/src/calibre/devices/misc.py @@ -34,7 +34,7 @@ class PALMPRE(USBMS): class AVANT(USBMS): name = 'Booq Avant Device Interface' - gui_name = 'bq Avant' + gui_name = 'Bq Avant' description = _('Communicate with the Bq Avant') author = 'Kovid Goyal' supported_platforms = ['windows', 'osx', 'linux'] @@ -521,7 +521,7 @@ class WOXTER(USBMS): class POCKETBOOK626(USBMS): name = 'PocketBook Touch Lux 2' - gui_name = 'PocketBook' + gui_name = 'PocketBook Touch Lux 2' description = _('Communicate with the PocketBook Touch Lux 2 and Inkpad X readers') author = 'Kovid Goyal' supported_platforms = ['windows', 'osx', 'linux'] diff --git a/src/calibre/devices/nook/driver.py b/src/calibre/devices/nook/driver.py index 1ffd5202d1..6073f37205 100644 --- a/src/calibre/devices/nook/driver.py +++ b/src/calibre/devices/nook/driver.py @@ -18,7 +18,7 @@ from calibre.utils.resources import get_image_path as I class NOOK(USBMS): name = 'Nook Device Interface' - gui_name = _('The Nook') + gui_name = 'B&N Nook' description = _('Communicate with the Nook e-book reader.') author = 'John Schember' icon = 'devices/nook.png' @@ -80,6 +80,7 @@ class NOOK(USBMS): class NOOK_COLOR(NOOK): name = 'Nook Color Device Interface' + gui_name = _('B&N Nook Color') description = _('Communicate with the Nook Color, TSR, Glowlight and Tablet e-book readers.') PRODUCT_ID = [ diff --git a/src/calibre/devices/prs505/driver.py b/src/calibre/devices/prs505/driver.py index 04bfe484bc..6b3d33a16b 100644 --- a/src/calibre/devices/prs505/driver.py +++ b/src/calibre/devices/prs505/driver.py @@ -20,7 +20,7 @@ from calibre.prints import debug_print class PRS505(USBMS): name = 'SONY Device Interface' - gui_name = 'SONY Reader' + gui_name = 'SONY PRS-500' description = _('Communicate with Sony e-book readers older than the' ' PRST1.') author = 'Kovid Goyal' diff --git a/src/calibre/devices/prst1/driver.py b/src/calibre/devices/prst1/driver.py index 970d99e5f1..f205c18d6c 100644 --- a/src/calibre/devices/prst1/driver.py +++ b/src/calibre/devices/prst1/driver.py @@ -38,7 +38,7 @@ class ImageWrapper: class PRST1(USBMS): name = 'SONY PRST1 and newer Device Interface' - gui_name = 'SONY Reader' + gui_name = 'SONY PRS-T1' description = _('Communicate with the PRST1 and newer SONY e-book readers') author = 'Kovid Goyal' supported_platforms = ['windows', 'osx', 'linux'] diff --git a/src/calibre/devices/usbms/device.py b/src/calibre/devices/usbms/device.py index f3d203c5af..1e49291bf0 100644 --- a/src/calibre/devices/usbms/device.py +++ b/src/calibre/devices/usbms/device.py @@ -150,7 +150,10 @@ class Device(DeviceConfig, DevicePlugin): bcd = cls.BCD return vid or 0, pid or 0, bcd or 0 vid, pid, bcd = get_representative_ids() - model_name = cls.get_gui_name() + try: + model_name = cls.get_gui_name() + except TypeError: # The WAYTEQ driver implements this as non classmethod + return () parts = model_name.split(' ', 1) manufacturer = '' if len(parts) > 1: diff --git a/src/calibre/gui2/dialogs/connect_to_folder.py b/src/calibre/gui2/dialogs/connect_to_folder.py index 1612767be4..e2043a4381 100644 --- a/src/calibre/gui2/dialogs/connect_to_folder.py +++ b/src/calibre/gui2/dialogs/connect_to_folder.py @@ -1,13 +1,130 @@ #!/usr/bin/env python # License: GPLv3 Copyright: 2025, Kovid Goyal -from qt.core import QVBoxLayout +from qt.core import QAbstractItemView, QGroupBox, QHBoxLayout, QIcon, QLabel, Qt, QToolButton, QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget -from calibre.gui2.widgets2 import Dialog +from calibre.customize.ui import usbms_plugins +from calibre.gui2 import Application, choose_dir, gprefs +from calibre.gui2.widgets2 import Dialog, HistoryLineEdit2 +from calibre.utils.icu import primary_sort_key + + +class ChooseFolder(QWidget): + + def __init__(self, parent=None): + super().__init__(parent) + self.folder_edit = fe = HistoryLineEdit2(parent=self) + fe.initialize('connect-to-folder') + fe.setPlaceholderText(_('Path to folder to connect to')) + if len(fe.history): + fe.setText(fe.history[0]) + self.browse_button = bb = QToolButton(self) + bb.setIcon(QIcon.ic('mimetypes/dir.png')) + bb.clicked.connect(self.browse) + self.l = l = QHBoxLayout(self) + l.setContentsMargins(0, 0, 0, 0) + l.addWidget(fe) + l.addWidget(bb) + + def browse(self): + ans = choose_dir(self, 'connect-to-folder-browse-history', _('Choose folder to connect to')) + if ans: + self.folder_edit.setText(ans) + + @property + def folder(self): + return self.folder_edit.text().strip() + + @folder.setter + def folder(self, val): + self.folder_edit.setText((val or '').strip()) + + def on_accept(self): + if self.folder: + self.folder_edit.save_history() class ConnectToFolder(Dialog): def __init__(self, parent=None): - super().__init__(_('Connect to folder'), 'connect-to-folderx', parent=parent) + super().__init__(_('Connect to folder'), 'connect-to-folder', parent=parent) + + def setup_ui(self): self.l = l = QVBoxLayout(self) + self.folder_chooser = fc = ChooseFolder(self) + l.addWidget(fc) + self.la = la = QLabel('

' + _( + 'Choose a device to connect as below. If no device is chosen a generic Folder device' + ' will be used.')) + self.la2 = la2 = QLabel('

' + _('WARNING: Connecting as a specific device will work only' + ' if the chosen folder above contains the actual files from an actual device, as the' + ' device drivers often expect to find certain device specific files. So only choose' + ' a device below if you have copied the files from a real device or mounted it at the' + ' chosen location.')) + la.setWordWrap(True), la2.setWordWrap(True) + l.addWidget(la) + self.devices_group = dg = QGroupBox(_('Connect as device'), self) + dg.setCheckable(True) + l.addWidget(dg) + l.addWidget(self.bb) + dg.l = l = QVBoxLayout(dg) + self.devices = d = QTreeWidget(self) + d.setHeaderHidden(True) + d.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection) + l.addWidget(la2) + l.addWidget(d) + + lcd = gprefs.get('last_connected_folder_as_device', None) + selected_device = None + man_map = {} + for cls in usbms_plugins(): + for model in cls.model_metadata(): + man_map.setdefault(model.manufacturer_name, []).append(model) + for manufacturer in sorted(man_map, key=primary_sort_key): + devs = man_map[manufacturer] + m = QTreeWidgetItem(d, 0) + m.setText(0, manufacturer) + m.setFlags(m.flags() & ~Qt.ItemFlag.ItemIsSelectable) + flags = m.flags() + devs.sort(key=lambda x: primary_sort_key(x.model_name)) + expanded = False + for dev in devs: + i = QTreeWidgetItem(m, 1) + i.setText(0, dev.model_name) + i.setData(0, Qt.ItemDataRole.UserRole, dev) + i.setFlags(i.flags() | Qt.ItemFlag.ItemNeverHasChildren) + if dev.settings_key == lcd: + i.setSelected(True) + expanded = True + selected_device = i + m.setExpanded(expanded) + if selected_device is not None: + d.scrollToItem(selected_device) + dg.setChecked(selected_device is not None) + + @property + def model_metadata(self): + if self.devices_group.isChecked(): + for m in range(self.devices.topLevelItemCount()): + man = self.devices.topLevelItem(m) + for i in range(man.childCount()): + item = man.child(i) + if item.isSelected(): + return item.data(0, Qt.ItemDataRole.UserRole) + + def accept(self): + self.folder_chooser.on_accept() + m = self.model_metadata + gprefs.set('last_connected_folder_as_device', None if m is None else m.settings_key) + return super().accept() + + @property + def ans(self): + return self.folder_chooser.folder, self.model_metadata + + +if __name__ == '__main__': + app = Application([]) + d = ConnectToFolder() + d.exec() + print(d.ans)