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