1#!/usr/local/bin/python3.8 2# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai 3 4 5__license__ = 'GPL v3' 6__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>' 7__docformat__ = 'restructuredtext en' 8 9import copy 10from collections import defaultdict 11 12from qt.core import Qt, QComboBox, QListWidgetItem 13 14from calibre.customize.ui import is_disabled 15from calibre.gui2 import error_dialog, question_dialog, warning_dialog 16from calibre.gui2.device import device_name_for_plugboards 17from calibre.gui2.dialogs.template_line_editor import TemplateLineEditor 18from calibre.gui2.preferences import ConfigWidgetBase, test_widget 19from calibre.gui2.preferences.plugboard_ui import Ui_Form 20from calibre.customize.ui import metadata_writers, device_plugins, disabled_device_plugins 21from calibre.library.save_to_disk import plugboard_any_format_value, \ 22 plugboard_any_device_value, plugboard_save_to_disk_value, \ 23 find_plugboard 24from calibre.srv.content import plugboard_content_server_value, plugboard_content_server_formats 25from calibre.gui2.email import plugboard_email_value, plugboard_email_formats 26from calibre.utils.formatter import validation_formatter 27from polyglot.builtins import native_string_type 28 29 30class ConfigWidget(ConfigWidgetBase, Ui_Form): 31 32 def genesis(self, gui): 33 self.gui = gui 34 self.db = gui.library_view.model().db 35 36 def initialize(self): 37 ConfigWidgetBase.initialize(self) 38 39 self.current_plugboards = copy.deepcopy(self.db.prefs.get('plugboards',{})) 40 self.current_device = None 41 self.current_format = None 42 43 if self.gui.device_manager.connected_device is not None: 44 self.device_label.setText(_('Device currently connected: ') + 45 self.gui.device_manager.connected_device.__class__.__name__) 46 else: 47 self.device_label.setText(_('Device currently connected: None')) 48 49 self.devices = ['', 'APPLE', 'FOLDER_DEVICE'] 50 self.disabled_devices = [] 51 self.device_to_formats_map = {} 52 for device in device_plugins(): 53 n = device_name_for_plugboards(device) 54 self.device_to_formats_map[n] = set(device.settings().format_map) 55 if getattr(device, 'CAN_DO_DEVICE_DB_PLUGBOARD', False): 56 self.device_to_formats_map[n].add('device_db') 57 if n not in self.devices: 58 self.devices.append(n) 59 60 for device in disabled_device_plugins(): 61 n = device_name_for_plugboards(device) 62 if n not in self.disabled_devices: 63 self.disabled_devices.append(n) 64 65 self.devices.sort(key=lambda x: x.lower()) 66 self.devices.insert(1, plugboard_save_to_disk_value) 67 self.devices.insert(1, plugboard_content_server_value) 68 self.device_to_formats_map[plugboard_content_server_value] = \ 69 plugboard_content_server_formats 70 self.devices.insert(1, plugboard_email_value) 71 self.device_to_formats_map[plugboard_email_value] = \ 72 plugboard_email_formats 73 self.devices.insert(1, plugboard_any_device_value) 74 self.new_device.addItems(self.devices) 75 76 self.formats = [''] 77 self.format_to_writers_map = defaultdict(list) 78 for w in metadata_writers(): 79 for f in w.file_types: 80 if f not in self.formats: 81 self.formats.append(f) 82 self.format_to_writers_map[f].append(w) 83 self.formats.append('device_db') 84 self.formats.sort() 85 self.formats.insert(1, plugboard_any_format_value) 86 self.new_format.addItems(self.formats) 87 88 self.dest_fields = ['', 89 'authors', 'author_sort', 'language', 'publisher', 90 'series', 'tags', 'title', 'title_sort', 'comments'] 91 92 self.source_widgets = [] 93 self.dest_widgets = [] 94 for i in range(0, len(self.dest_fields)-1): 95 w = TemplateLineEditor(self) 96 self.source_widgets.append(w) 97 self.fields_layout.addWidget(w, 5+i, 0, 1, 1) 98 w = QComboBox(self) 99 self.dest_widgets.append(w) 100 self.fields_layout.addWidget(w, 5+i, 1, 1, 1) 101 102 self.edit_device.currentIndexChanged[native_string_type].connect(self.edit_device_changed) 103 self.edit_format.currentIndexChanged[native_string_type].connect(self.edit_format_changed) 104 self.new_device.currentIndexChanged[native_string_type].connect(self.new_device_changed) 105 self.new_format.currentIndexChanged[native_string_type].connect(self.new_format_changed) 106 self.existing_plugboards.itemClicked.connect(self.existing_pb_clicked) 107 self.ok_button.clicked.connect(self.ok_clicked) 108 self.del_button.clicked.connect(self.del_clicked) 109 110 self.refilling = False 111 self.refill_all_boxes() 112 113 def clear_fields(self, edit_boxes=False, new_boxes=False): 114 self.ok_button.setEnabled(False) 115 self.del_button.setEnabled(False) 116 for w in self.source_widgets: 117 w.clear() 118 for w in self.dest_widgets: 119 w.clear() 120 if edit_boxes: 121 self.edit_device.setCurrentIndex(0) 122 self.edit_format.setCurrentIndex(0) 123 if new_boxes: 124 self.new_device.setCurrentIndex(0) 125 self.new_format.setCurrentIndex(0) 126 127 def set_fields(self): 128 self.ok_button.setEnabled(True) 129 self.del_button.setEnabled(True) 130 for w in self.source_widgets: 131 w.clear() 132 for w in self.dest_widgets: 133 w.addItems(self.dest_fields) 134 135 def set_field(self, i, src, dst): 136 self.source_widgets[i].setText(src) 137 idx = self.dest_fields.index(dst) 138 self.dest_widgets[i].setCurrentIndex(idx) 139 140 def edit_device_changed(self, txt): 141 self.current_device = None 142 if txt == '': 143 self.clear_fields(new_boxes=False) 144 return 145 self.clear_fields(new_boxes=True) 146 self.current_device = str(txt) 147 fpb = self.current_plugboards.get(self.current_format, None) 148 if fpb is None: 149 print('edit_device_changed: none format!') 150 return 151 dpb = fpb.get(self.current_device, None) 152 if dpb is None: 153 print('edit_device_changed: none device!') 154 return 155 self.set_fields() 156 for i,op in enumerate(dpb): 157 self.set_field(i, op[0], op[1]) 158 self.ok_button.setEnabled(True) 159 self.del_button.setEnabled(True) 160 161 def edit_format_changed(self, txt): 162 self.edit_device.setCurrentIndex(0) 163 self.current_device = None 164 self.current_format = None 165 if txt == '': 166 self.clear_fields(new_boxes=False) 167 return 168 self.clear_fields(new_boxes=True) 169 txt = str(txt) 170 fpb = self.current_plugboards.get(txt, None) 171 if fpb is None: 172 print('edit_format_changed: none editable format!') 173 return 174 self.current_format = txt 175 self.check_if_writer_disabled(txt) 176 devices = [''] 177 for d in fpb: 178 devices.append(d) 179 self.edit_device.clear() 180 self.edit_device.addItems(devices) 181 182 def check_if_writer_disabled(self, format_name): 183 if format_name in ['device_db', plugboard_any_format_value]: 184 return 185 show_message = True 186 for writer in self.format_to_writers_map[format_name]: 187 if not is_disabled(writer): 188 show_message = False 189 if show_message: 190 warning_dialog(self, '', 191 _('That format has no metadata writers enabled. A plugboard ' 192 'will probably have no effect.'), 193 show=True) 194 195 def new_device_changed(self, txt): 196 self.current_device = None 197 if txt == '': 198 self.clear_fields(edit_boxes=False) 199 return 200 self.clear_fields(edit_boxes=True) 201 self.current_device = str(txt) 202 203 if self.current_format in self.current_plugboards and \ 204 self.current_device in self.current_plugboards[self.current_format]: 205 error_dialog(self, '', 206 _('That format and device already has a plugboard.'), 207 show=True) 208 self.new_device.setCurrentIndex(0) 209 return 210 211 # If we have a specific format/device combination, check if a more 212 # general combination matches. 213 if self.current_format != plugboard_any_format_value and \ 214 self.current_device != plugboard_any_device_value: 215 if find_plugboard(self.current_device, self.current_format, 216 self.current_plugboards): 217 if not question_dialog(self.gui, 218 _('Possibly override plugboard?'), 219 _('A more general plugboard already exists for ' 220 'that format and device. ' 221 'Are you sure you want to add the new plugboard?')): 222 self.new_device.setCurrentIndex(0) 223 return 224 225 # If we have a specific format, check if we are adding a possibly- 226 # covered plugboard 227 if self.current_format != plugboard_any_format_value: 228 if self.current_format in self.current_plugboards: 229 if self.current_device == plugboard_any_device_value: 230 if not question_dialog(self.gui, 231 _('Add possibly overridden plugboard?'), 232 _('More specific device plugboards exist for ' 233 'that format. ' 234 'Are you sure you want to add the new plugboard?')): 235 self.new_device.setCurrentIndex(0) 236 return 237 # We are adding an 'any format' entry. Check if we are adding a specific 238 # device and if so, does some other plugboard match that device. 239 elif self.current_device != plugboard_any_device_value: 240 for fmt in self.current_plugboards: 241 if find_plugboard(self.current_device, fmt, self.current_plugboards): 242 if not question_dialog(self.gui, 243 _('Really add plugboard?'), 244 _('A different plugboard matches that format and ' 245 'device combination. ' 246 'Are you sure you want to add the new plugboard?')): 247 self.new_device.setCurrentIndex(0) 248 return 249 # We are adding an any format/any device entry, which will be overridden 250 # by any other entry. Ask if such entries exist. 251 elif len(self.current_plugboards): 252 if not question_dialog(self.gui, 253 _('Add possibly overridden plugboard?'), 254 _('More specific format and device plugboards ' 255 'already exist. ' 256 'Are you sure you want to add the new plugboard?')): 257 self.new_device.setCurrentIndex(0) 258 return 259 260 if self.current_format != plugboard_any_format_value and \ 261 self.current_device in self.device_to_formats_map: 262 allowable_formats = self.device_to_formats_map[self.current_device] 263 if self.current_format not in allowable_formats: 264 error_dialog(self, '', 265 _('The {0} device does not support the {1} format.'). 266 format(self.current_device, self.current_format), show=True) 267 self.new_device.setCurrentIndex(0) 268 return 269 270 if self.current_format == plugboard_any_format_value and \ 271 self.current_device == plugboard_content_server_value: 272 warning_dialog(self, '', 273 _('The {0} device supports only the {1} format(s).'). 274 format(plugboard_content_server_value, 275 ', '.join(plugboard_content_server_formats)), show=True) 276 277 self.set_fields() 278 279 def new_format_changed(self, txt): 280 self.current_format = None 281 self.current_device = None 282 self.new_device.setCurrentIndex(0) 283 if txt: 284 self.clear_fields(edit_boxes=True) 285 self.current_format = str(txt) 286 self.check_if_writer_disabled(self.current_format) 287 else: 288 self.clear_fields(edit_boxes=False) 289 290 def ok_clicked(self): 291 pb = [] 292 comments_in_dests = False 293 for i in range(0, len(self.source_widgets)): 294 s = str(self.source_widgets[i].text()) 295 if s: 296 d = self.dest_widgets[i].currentIndex() 297 if d != 0: 298 try: 299 validation_formatter.validate(s) 300 except Exception as err: 301 error_dialog(self, _('Invalid template'), 302 '<p>'+_('The template %s is invalid:')%s + 303 '<br>'+str(err), show=True) 304 return 305 pb.append((s, self.dest_fields[d])) 306 comments_in_dests = comments_in_dests or self.dest_fields[d] == 'comments' 307 else: 308 error_dialog(self, _('Invalid destination'), 309 '<p>'+_('The destination field cannot be blank'), 310 show=True) 311 return 312 if len(pb) == 0: 313 if self.current_format in self.current_plugboards: 314 fpb = self.current_plugboards[self.current_format] 315 if self.current_device in fpb: 316 del fpb[self.current_device] 317 if len(fpb) == 0: 318 del self.current_plugboards[self.current_format] 319 else: 320 if comments_in_dests and not question_dialog(self.gui, _('Plugboard modifies comments'), 321 _('This plugboard modifies the comments metadata. ' 322 'If the comments are set to invalid HTML, it could cause problems on the device. ' 323 'Are you sure you wish to save this plugboard?' 324 ), 325 skip_dialog_name='plugboard_comments_in_dests' 326 ): 327 return 328 if self.current_format not in self.current_plugboards: 329 self.current_plugboards[self.current_format] = {} 330 fpb = self.current_plugboards[self.current_format] 331 fpb[self.current_device] = pb 332 self.changed_signal.emit() 333 self.refill_all_boxes() 334 335 def del_clicked(self): 336 if self.current_format in self.current_plugboards: 337 fpb = self.current_plugboards[self.current_format] 338 if self.current_device in fpb: 339 del fpb[self.current_device] 340 if len(fpb) == 0: 341 del self.current_plugboards[self.current_format] 342 self.changed_signal.emit() 343 self.refill_all_boxes() 344 345 def existing_pb_clicked(self, qitem): 346 item = qitem.data(Qt.ItemDataRole.UserRole) 347 if (qitem.flags() & Qt.ItemFlag.ItemIsEnabled): 348 self.edit_format.setCurrentIndex(self.edit_format.findText(item[0])) 349 self.edit_device.setCurrentIndex(self.edit_device.findText(item[1])) 350 else: 351 warning_dialog(self, '', 352 _('The {0} device plugin is disabled.').format(item[1]), 353 show=True) 354 355 def refill_all_boxes(self): 356 if self.refilling: 357 return 358 self.refilling = True 359 self.current_device = None 360 self.current_format = None 361 self.clear_fields(new_boxes=True) 362 self.edit_format.clear() 363 self.edit_format.addItem('') 364 for format_ in self.current_plugboards: 365 self.edit_format.addItem(format_) 366 self.edit_format.setCurrentIndex(0) 367 self.edit_device.clear() 368 self.ok_button.setEnabled(False) 369 self.del_button.setEnabled(False) 370 self.existing_plugboards.clear() 371 for f in self.formats: 372 if f not in self.current_plugboards: 373 continue 374 for d in sorted(self.devices + self.disabled_devices, key=lambda x:x.lower()): 375 if d not in self.current_plugboards[f]: 376 continue 377 ops = [] 378 for op in self.current_plugboards[f][d]: 379 ops.append('([' + op[0] + '] -> ' + op[1] + ')') 380 txt = '%s:%s = %s\n'%(f, d, ', '.join(ops)) 381 item = QListWidgetItem(txt) 382 item.setData(Qt.ItemDataRole.UserRole, (f, d)) 383 if d in self.disabled_devices: 384 item.setFlags(item.flags() & ~Qt.ItemFlag.ItemIsEnabled) 385 self.existing_plugboards.addItem(item) 386 self.refilling = False 387 388 def restore_defaults(self): 389 ConfigWidgetBase.restore_defaults(self) 390 self.current_plugboards = {} 391 self.refill_all_boxes() 392 self.changed_signal.emit() 393 394 def commit(self): 395 self.db.new_api.set_pref('plugboards', self.current_plugboards) 396 return ConfigWidgetBase.commit(self) 397 398 def refresh_gui(self, gui): 399 pass 400 401 402if __name__ == '__main__': 403 from qt.core import QApplication 404 app = QApplication([]) 405 test_widget('Import/Export', 'Plugboard') 406