1#!/usr/local/bin/python3.8 2# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:fdm=marker:ai 3 4__license__ = 'GPL v3' 5__copyright__ = '2015, Kovid Goyal <kovid at kovidgoyal.net>' 6__docformat__ = 'restructuredtext en' 7 8import weakref, textwrap 9 10from qt.core import ( 11 QWidget, QLabel, QTabWidget, QGridLayout, QLineEdit, QVBoxLayout, 12 QGroupBox, QComboBox, QSizePolicy, QDialog, QDialogButtonBox, QCheckBox, 13 QSpacerItem) 14 15from calibre.ebooks import BOOK_EXTENSIONS 16from calibre.gui2.device_drivers.mtp_config import (FormatsConfig, TemplateConfig) 17from calibre.devices.usbms.driver import debug_print 18 19 20def wrap_msg(msg): 21 return textwrap.fill(msg.strip(), 100) 22 23 24def setToolTipFor(widget, tt): 25 widget.setToolTip(wrap_msg(tt)) 26 27 28def create_checkbox(title, tt, state): 29 cb = QCheckBox(title) 30 cb.setToolTip(wrap_msg(tt)) 31 cb.setChecked(bool(state)) 32 return cb 33 34 35class TabbedDeviceConfig(QTabWidget): 36 """ 37 This is a generic Tabbed Device config widget. It designed for devices with more 38 complex configuration. But, it is backwards compatible to the standard device 39 configuration widget. 40 41 The configuration made up of two default tabs plus extra tabs as needed for the 42 device. The extra tabs are defined as part of the subclass of this widget for 43 the device. 44 45 The two default tabs are the "File Formats" and "Extra Customization". These 46 tabs are the same as the two sections of the standard device configuration 47 widget. The second of these tabs will only be created if the device driver has 48 extra configuration options. All options on these tabs work the same way as for 49 the standard device configuration widget. 50 51 When implementing a subclass for a device driver, create tabs, subclassed from 52 DeviceConfigTab, for each set of options. Within the tabs, group boxes, subclassed 53 from DeviceOptionsGroupBox, are created to further group the options. The group 54 boxes can be coded to support any control type and dependencies between them. 55 """ 56 57 def __init__(self, device_settings, all_formats, supports_subdirs, 58 must_read_metadata, supports_use_author_sort, 59 extra_customization_message, device, 60 extra_customization_choices=None, parent=None): 61 QTabWidget.__init__(self, parent) 62 self._device = weakref.ref(device) 63 64 self.device_settings = device_settings 65 self.all_formats = set(all_formats) 66 self.supports_subdirs = supports_subdirs 67 self.must_read_metadata = must_read_metadata 68 self.supports_use_author_sort = supports_use_author_sort 69 self.extra_customization_message = extra_customization_message 70 self.extra_customization_choices = extra_customization_choices 71 72 try: 73 self.device_name = device.get_gui_name() 74 except TypeError: 75 self.device_name = getattr(device, 'gui_name', None) or _('Device') 76 77 if device.USER_CAN_ADD_NEW_FORMATS: 78 self.all_formats = set(self.all_formats) | set(BOOK_EXTENSIONS) 79 80 self.base = QWidget(self) 81# self.insertTab(0, self.base, _('Configure %s') % self.device.current_friendly_name) 82 self.insertTab(0, self.base, _("File formats")) 83 l = self.base.l = QGridLayout(self.base) 84 self.base.setLayout(l) 85 86 self.formats = FormatsConfig(self.all_formats, device_settings.format_map) 87 if device.HIDE_FORMATS_CONFIG_BOX: 88 self.formats.hide() 89 90 self.opt_use_subdirs = create_checkbox( 91 _("Use sub-folders"), 92 _('Place files in sub-folders if the device supports them'), 93 device_settings.use_subdirs 94 ) 95 self.opt_read_metadata = create_checkbox( 96 _("Read metadata from files on device"), 97 _('Read metadata from files on device'), 98 device_settings.read_metadata 99 ) 100 101 self.template = TemplateConfig(device_settings.save_template) 102 self.opt_use_author_sort = create_checkbox( 103 _("Use author sort for author"), 104 _("Use author sort for author"), 105 device_settings.read_metadata 106 ) 107 self.opt_use_author_sort.setObjectName("opt_use_author_sort") 108 self.base.la = la = QLabel(_( 109 'Choose the formats to send to the %s')%self.device_name) 110 la.setWordWrap(True) 111 112 l.addWidget(la, 1, 0, 1, 1) 113 l.addWidget(self.formats, 2, 0, 1, 1) 114 l.addWidget(self.opt_read_metadata, 3, 0, 1, 1) 115 l.addWidget(self.opt_use_subdirs, 4, 0, 1, 1) 116 l.addWidget(self.opt_use_author_sort, 5, 0, 1, 1) 117 l.addWidget(self.template, 6, 0, 1, 1) 118 l.setRowStretch(2, 10) 119 120 if device.HIDE_FORMATS_CONFIG_BOX: 121 self.formats.hide() 122 123 if supports_subdirs: 124 self.opt_use_subdirs.setChecked(device_settings.use_subdirs) 125 else: 126 self.opt_use_subdirs.hide() 127 if not must_read_metadata: 128 self.opt_read_metadata.setChecked(device_settings.read_metadata) 129 else: 130 self.opt_read_metadata.hide() 131 if supports_use_author_sort: 132 self.opt_use_author_sort.setChecked(device_settings.use_author_sort) 133 else: 134 self.opt_use_author_sort.hide() 135 136 self.extra_tab = ExtraCustomization(self.extra_customization_message, 137 self.extra_customization_choices, 138 self.device_settings) 139 # Only display the extra customization tab if there are options on it. 140 if self.extra_tab.has_extra_customizations: 141 self.addTab(self.extra_tab, _('Extra customization')) 142 143 self.setCurrentIndex(0) 144 145 def addDeviceTab(self, tab, label): 146 ''' 147 This is used to add a new tab for the device config. The new tab will always be added 148 as immediately before the "Extra Customization" tab. 149 ''' 150 extra_tab_pos = self.indexOf(self.extra_tab) 151 self.insertTab(extra_tab_pos, tab, label) 152 153 def __getattr__(self, attr_name): 154 "If the object doesn't have an attribute, then check each tab." 155 try: 156 return super().__getattr__(attr_name) 157 except AttributeError as ae: 158 for i in range(0, self.count()): 159 atab = self.widget(i) 160 try: 161 return getattr(atab, attr_name) 162 except AttributeError: 163 pass 164 raise ae 165 166 @property 167 def device(self): 168 return self._device() 169 170 def format_map(self): 171 return self.formats.format_map 172 173 def use_subdirs(self): 174 return self.opt_use_subdirs.isChecked() 175 176 def read_metadata(self): 177 return self.opt_read_metadata.isChecked() 178 179 def use_author_sort(self): 180 return self.opt_use_author_sort.isChecked() 181 182 @property 183 def opt_save_template(self): 184 # Really shouldn't be accessing the template this way 185 return self.template.t 186 187 def text(self): 188 # Really shouldn't be accessing the template this way 189 return self.template.t.text() 190 191 @property 192 def opt_extra_customization(self): 193 return self.extra_tab.opt_extra_customization 194 195 @property 196 def label(self): 197 return self.opt_save_template 198 199 def validate(self): 200 if hasattr(self, 'formats'): 201 if not self.formats.validate(): 202 return False 203 if not self.template.validate(): 204 return False 205 return True 206 207 def commit(self): 208 debug_print("TabbedDeviceConfig::commit: start") 209 p = self.device._configProxy() 210 211 p['format_map'] = self.formats.format_map 212 p['use_subdirs'] = self.use_subdirs() 213 p['read_metadata'] = self.read_metadata() 214 p['save_template'] = self.template.template 215 p['extra_customization'] = self.extra_tab.extra_customization() 216 217 return p 218 219 220class DeviceConfigTab(QWidget): # {{{ 221 ''' 222 This is an abstraction for a tab in the configuration. The main reason for it is to 223 abstract the properties of the configuration tab. When a property is accessed, it 224 will iterate over all known widgets looking for the property. 225 ''' 226 227 def __init__(self, parent=None): 228 QWidget.__init__(self) 229 self.parent = parent 230 231 self.device_widgets = [] 232 233 def addDeviceWidget(self, widget): 234 self.device_widgets.append(widget) 235 236 def __getattr__(self, attr_name): 237 try: 238 return super().__getattr__(attr_name) 239 except AttributeError as ae: 240 for awidget in self.device_widgets: 241 try: 242 return getattr(awidget, attr_name) 243 except AttributeError: 244 pass 245 raise ae 246 247 248class ExtraCustomization(DeviceConfigTab): # {{{ 249 250 def __init__(self, extra_customization_message, extra_customization_choices, device_settings): 251 super().__init__() 252 253 debug_print("ExtraCustomization.__init__ - extra_customization_message=", extra_customization_message) 254 debug_print("ExtraCustomization.__init__ - extra_customization_choices=", extra_customization_choices) 255 debug_print("ExtraCustomization.__init__ - device_settings.extra_customization=", device_settings.extra_customization) 256 debug_print("ExtraCustomization.__init__ - device_settings=", device_settings) 257 self.extra_customization_message = extra_customization_message 258 259 self.l = QVBoxLayout(self) 260 self.setLayout(self.l) 261 262 options_group = QGroupBox(_("Extra driver customization options"), self) 263 self.l.addWidget(options_group) 264 self.extra_layout = QGridLayout() 265 self.extra_layout.setObjectName("extra_layout") 266 options_group.setLayout(self.extra_layout) 267 268 if extra_customization_message: 269 extra_customization_choices = extra_customization_choices or {} 270 271 def parse_msg(m): 272 msg, _, tt = m.partition(':::') if m else ('', '', '') 273 return msg.strip(), textwrap.fill(tt.strip(), 100) 274 275 if isinstance(extra_customization_message, list): 276 self.opt_extra_customization = [] 277 if len(extra_customization_message) > 6: 278 row_func = lambda x, y: ((x//2) * 2) + y 279 col_func = lambda x: x%2 280 else: 281 row_func = lambda x, y: x*2 + y 282 col_func = lambda x: 0 283 284 for i, m in enumerate(extra_customization_message): 285 label_text, tt = parse_msg(m) 286 if not label_text: 287 self.opt_extra_customization.append(None) 288 continue 289 if isinstance(device_settings.extra_customization[i], bool): 290 self.opt_extra_customization.append(QCheckBox(label_text)) 291 self.opt_extra_customization[-1].setToolTip(tt) 292 self.opt_extra_customization[i].setChecked(bool(device_settings.extra_customization[i])) 293 elif i in extra_customization_choices: 294 cb = QComboBox(self) 295 self.opt_extra_customization.append(cb) 296 l = QLabel(label_text) 297 l.setToolTip(tt), cb.setToolTip(tt), l.setBuddy(cb), cb.setToolTip(tt) 298 for li in sorted(extra_customization_choices[i]): 299 self.opt_extra_customization[i].addItem(li) 300 cb.setCurrentIndex(max(0, cb.findText(device_settings.extra_customization[i]))) 301 else: 302 self.opt_extra_customization.append(QLineEdit(self)) 303 l = QLabel(label_text) 304 l.setToolTip(tt) 305 self.opt_extra_customization[i].setToolTip(tt) 306 l.setBuddy(self.opt_extra_customization[i]) 307 l.setWordWrap(True) 308 self.opt_extra_customization[i].setText(device_settings.extra_customization[i]) 309 self.opt_extra_customization[i].setCursorPosition(0) 310 self.extra_layout.addWidget(l, row_func(i + 2, 0), col_func(i)) 311 self.extra_layout.addWidget(self.opt_extra_customization[i], 312 row_func(i + 2, 1), col_func(i)) 313 spacerItem1 = QSpacerItem(10, 10, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding) 314 self.extra_layout.addItem(spacerItem1, row_func(i + 2 + 2, 1), 0, 1, 2) 315 self.extra_layout.setRowStretch(row_func(i + 2 + 2, 1), 2) 316 else: 317 self.opt_extra_customization = QLineEdit() 318 label_text, tt = parse_msg(extra_customization_message) 319 l = QLabel(label_text) 320 l.setToolTip(tt) 321 l.setBuddy(self.opt_extra_customization) 322 l.setWordWrap(True) 323 if device_settings.extra_customization: 324 self.opt_extra_customization.setText(device_settings.extra_customization) 325 self.opt_extra_customization.setCursorPosition(0) 326 self.opt_extra_customization.setCursorPosition(0) 327 self.extra_layout.addWidget(l, 0, 0) 328 self.extra_layout.addWidget(self.opt_extra_customization, 1, 0) 329 330 def extra_customization(self): 331 ec = [] 332 if self.extra_customization_message: 333 if isinstance(self.extra_customization_message, list): 334 for i in range(0, len(self.extra_customization_message)): 335 if self.opt_extra_customization[i] is None: 336 ec.append(None) 337 continue 338 if hasattr(self.opt_extra_customization[i], 'isChecked'): 339 ec.append(self.opt_extra_customization[i].isChecked()) 340 elif hasattr(self.opt_extra_customization[i], 'currentText'): 341 ec.append(str(self.opt_extra_customization[i].currentText()).strip()) 342 else: 343 ec.append(str(self.opt_extra_customization[i].text()).strip()) 344 else: 345 ec = str(self.opt_extra_customization.text()).strip() 346 if not ec: 347 ec = None 348 349 return ec 350 351 @property 352 def has_extra_customizations(self): 353 debug_print("ExtraCustomization::has_extra_customizations - self.extra_customization_message", self.extra_customization_message) 354 return self.extra_customization_message and len(self.extra_customization_message) > 0 355 356# }}} 357 358 359class DeviceOptionsGroupBox(QGroupBox): 360 """ 361 This is a container for the individual options for a device driver. 362 """ 363 364 def __init__(self, parent, device=None, title=_("Unknown")): 365 QGroupBox.__init__(self, parent) 366 367 self.device = device 368 self.setTitle(title) 369 370 371if __name__ == '__main__': 372 from calibre.gui2 import Application 373 from calibre.devices.kobo.driver import KOBO 374 from calibre.devices.scanner import DeviceScanner 375 s = DeviceScanner() 376 s.scan() 377 app = Application([]) 378 dev = KOBO(None) 379 debug_print("KOBO:", KOBO) 380# dev.startup() 381# cd = dev.detect_managed_devices(s.devices) 382# dev.open(cd, 'test') 383 cw = dev.config_widget() 384 d = QDialog() 385 d.l = QVBoxLayout() 386 d.setLayout(d.l) 387 d.l.addWidget(cw) 388 bb = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok|QDialogButtonBox.StandardButton.Cancel) 389 d.l.addWidget(bb) 390 bb.accepted.connect(d.accept) 391 bb.rejected.connect(d.reject) 392 if d.exec() == QDialog.DialogCode.Accepted: 393 cw.commit() 394 dev.shutdown() 395