1from __future__ import division
2
3import os
4import sys
5import pymol
6
7from pymol.Qt import QtGui, QtCore, QtWidgets
8from pymol.Qt.utils import getSaveFileNameWithExt, AsyncFunc
9from pymol.Qt.utils import PopupOnException
10
11if sys.version_info[0] < 3:
12    import urllib
13else:
14    import urllib.request as urllib
15
16
17def load_dialog(parent, fname, **kwargs):
18    '''
19    Load a file into PyMOL. May show a file format specific options
20    dialog (e.g. trajectory loading dialog). Registers the filename
21    in the "recent files" history.
22    '''
23    if '://' not in fname:
24        parent.initialdir = os.path.dirname(fname)
25
26    parent.recent_filenames_add(fname)
27
28    format = pymol.importing.filename_to_format(fname)[2]
29
30    if fname[-4:] in ['.dcd', '.dtr', '.xtc', '.trr']:
31        load_traj_dialog(parent, fname)
32    elif format in ('aln', 'fasta'):
33        load_aln_dialog(parent, fname)
34    elif format == 'mae':
35        load_mae_dialog(parent, fname)
36    elif format == 'ccp4':
37        load_map_dialog(parent, fname, 'ccp4')
38    elif format == 'brix':
39        load_map_dialog(parent, fname, 'o')
40    elif format == 'mtz':
41        load_mtz_dialog(parent, fname)
42    else:
43        if format in ('pse', 'psw') and not ask_partial(parent, kwargs, fname):
44            return
45
46        if format in ('pml', 'py', 'pym'):
47            parent.cmd.cd(parent.initialdir, quiet=0)
48
49        try:
50            parent.cmd.load(fname, quiet=0, **kwargs)
51        except BaseException as e:
52            QtWidgets.QMessageBox.critical(parent, "Error", str(e))
53            return
54
55        # auto-load desmond trajectory
56        if fname.endswith('-out.cms'):
57            for suffix in [
58                ('_trj', 'clickme.dtr'),
59                ('.xtc',),
60            ]:
61                traj = os.path.join(fname[:-8] + suffix[0], *suffix[1:])
62                if os.path.exists(traj):
63                    load_traj_dialog(parent, traj)
64                    break
65
66    return True
67
68
69def ask_partial(parent, kwargs, fname):
70    if kwargs.get('partial', 0) or not parent.cmd.get_names():
71        return True
72
73    form = parent.load_form('askpartial')
74    form.check_rename.setChecked(parent.cmd.get_setting_boolean(
75        'auto_rename_duplicate_objects'))
76
77    if not form._dialog.exec_():
78        return False
79
80    if form.check_partial.isChecked():
81        kwargs['partial'] = 1
82        parent.cmd.set('auto_rename_duplicate_objects',
83                form.check_rename.isChecked(), quiet=0)
84    elif form.check_new.isChecked():
85        parent.new_window([fname])
86        return False
87
88    return True
89
90
91def load_traj_dialog(parent, filename):
92    '''Open a trajectory loading dialog'''
93    names = parent.cmd.get_object_list()
94
95    if not names:
96        msg = "To load a trajectory, you first need to load a molecular object"  #noqa
97        QtWidgets.QMessageBox.warning(parent, "Warning", msg)
98        return
99
100    form = parent.load_form('load_traj')
101    form.input_object.addItems(names)
102    form.input_object.setCurrentIndex(form.input_object.count() - 1)
103
104    def get_command(*args):
105        command = ''
106        if form.input_dbm3.isChecked():
107            command += 'set defer_builds_mode, 3\n'
108        command += ('load_traj \\\n'
109                   '    %s, \\\n'
110                   '    %s, %d, \\\n'
111                   '    start=%d, stop=%d, interval=%d' % (
112                       filename,
113                       form.input_object.currentText(),
114                       form.input_state.value(),
115                       form.input_start.value(),
116                       form.input_stop.value(),
117                       form.input_interval.value()))
118        return command
119
120    def update_output_command(*args):
121        form.output_command.setText(get_command())
122
123    def run():
124        parent.cmd.do(get_command())
125        form._dialog.close()
126
127    # hook up events
128    form.input_object.currentIndexChanged.connect(update_output_command)
129    form.input_state.valueChanged.connect(update_output_command)
130    form.input_start.valueChanged.connect(update_output_command)
131    form.input_stop.valueChanged.connect(update_output_command)
132    form.input_interval.valueChanged.connect(update_output_command)
133    form.input_dbm3.toggled.connect(update_output_command)
134    form.button_ok.clicked.connect(run)
135
136    update_output_command()
137    form._dialog.setModal(True)
138    form._dialog.show()
139
140
141def load_mtz_dialog(parent, filename):
142    from pymol import headering
143
144    _fileData = headering.MTZHeader(filename)
145
146    FCols = _fileData.getColumnsOfType("F") + \
147            _fileData.getColumnsOfType("G")
148    PCols = _fileData.getColumnsOfType("P")
149    WCols = _fileData.getColumnsOfType("W") + \
150            _fileData.getColumnsOfType("Q")
151    _2FC, _2PC, _looksLike = _fileData.guessCols("2FoFc")
152    _FC, _PC, _looksLike = _fileData.guessCols("FoFc")
153
154    form = parent.load_form('load_mtz')
155    form.input_amplitudes.addItems(FCols)
156    form.input_phases.addItems(PCols)
157    form.input_weights.addItem("")
158    form.input_weights.addItems(WCols)
159    form.input_prefix.setFocus(),
160
161    for col in [_2FC, _FC]:
162        if col in FCols:
163            form.input_amplitudes.setCurrentIndex(FCols.index(col))
164            break
165    for col in [_2PC, _PC]:
166        if col in PCols:
167            form.input_phases.setCurrentIndex(PCols.index(col))
168            break
169
170    if _fileData.reso_min is not None:
171        form.input_reso_min.setValue(_fileData.reso_min)
172    if _fileData.reso_max is not None:
173        form.input_reso_max.setValue(_fileData.reso_max)
174
175    def run():
176        try:
177            parent.cmd.load_mtz(filename,
178                    form.input_prefix.text(),
179                    form.input_amplitudes.currentText(),
180                    form.input_phases.currentText(),
181                    form.input_weights.currentText(),
182                    form.input_reso_min.value(),
183                    form.input_reso_max.value(),
184                    quiet=0)
185        except BaseException as e:
186            QtWidgets.QMessageBox.critical(parent, "Error", str(e))
187
188    form._dialog.accepted.connect(run)
189    form._dialog.setModal(True)
190    form._dialog.show()
191
192
193def load_aln_dialog(parent, filename):
194    _self = parent.cmd
195
196    import numpy
197    import difflib
198    import pymol.seqalign as seqalign
199
200    try:
201        alignment = seqalign.aln_magic_read(filename)
202    except ValueError:
203        # fails for fasta files which don't contain alignments
204        _self.load(filename)
205        return
206
207    # alignment record ids and PyMOL model names
208    ids = [rec.id for rec in alignment]
209    ids_remain = list(ids)
210    models = _self.get_object_list()
211    models_remain = list(models)
212    mapping = {}
213
214    N = len(ids)
215    M = len(models)
216
217    # ids -> models similarity matrix
218    similarity = numpy.zeros((N, M))
219    for i in range(N):
220        for j in range(M):
221            similarity[i, j] = difflib.SequenceMatcher(None,
222                    ids[i], models[j], False).ratio()
223
224    # guess mapping
225    for _ in range(min(N, M)):
226        i, j = numpy.unravel_index(similarity.argmax(), similarity.shape)
227        mapping[ids_remain.pop(i)] = models_remain.pop(j)
228        similarity = numpy.delete(similarity, i, axis=0)
229        similarity = numpy.delete(similarity, j, axis=1)
230
231    form = parent.load_form('load_aln')
232    comboboxes = {}
233
234    # mapping GUI
235    for row, rec_id in enumerate(ids, 1):
236        label = QtWidgets.QLabel(rec_id, form._dialog)
237        combobox = QtWidgets.QComboBox(form._dialog)
238        combobox.addItem("")
239        combobox.addItems(models)
240        combobox.setCurrentText(mapping.get(rec_id, ""))
241        form.layout_mapping.addWidget(label, row, 0)
242        form.layout_mapping.addWidget(combobox, row, 1)
243        comboboxes[rec_id] = combobox
244
245    def run():
246        mapping = dict((rec_id, combobox.currentText())
247                for (rec_id, combobox) in comboboxes.items())
248        seqalign.load_aln_multi(filename, mapping=mapping, _self=_self)
249        form._dialog.close()
250
251    # hook up events
252    form.button_ok.clicked.connect(run)
253    form.button_cancel.clicked.connect(form._dialog.close)
254
255    form._dialog.setModal(True)
256    form._dialog.show()
257
258
259def load_mae_dialog(parent, filename):
260    form = parent.load_form('load_mae')
261
262    form.input_object_name.setPlaceholderText(
263            pymol.importing.filename_to_objectname(filename))
264    form.input_object_props.setText(parent.cmd.get('load_object_props_default') or '*')
265    form.input_atom_props.setText(parent.cmd.get('load_atom_props_default') or '*')
266
267    def get_command(*args):
268        command = ('load \\\n    %s' % (filename))
269        name = form.input_object_name.text()
270        if name:
271            command += ', \\\n    ' + name
272        command += ', \\\n    mimic=' + ('1' if form.input_mimic.isChecked() else '0')
273        command += (
274                ', \\\n    object_props=%s'
275                ', \\\n    atom_props=%s' % (
276                    form.input_object_props.text(),
277                    form.input_atom_props.text()))
278        multiplex = [-2, 0, 1][form.input_multiplex.currentIndex()]
279        if multiplex != -2:
280            command += ', \\\n    multiplex={}'.format(multiplex)
281        return command
282
283    def update_output_command(*args):
284        form.output_command.setText(get_command())
285
286    def run():
287        parent.cmd.do(get_command())
288        form._dialog.close()
289
290    # hook up events
291    form.input_multiplex.currentIndexChanged.connect(update_output_command)
292    form.input_mimic.stateChanged.connect(update_output_command)
293    form.input_object_name.textChanged.connect(update_output_command)
294    form.input_object_props.textChanged.connect(update_output_command)
295    form.input_atom_props.textChanged.connect(update_output_command)
296    form.button_ok.clicked.connect(run)
297
298    update_output_command()
299    form._dialog.setModal(True)
300    form._dialog.show()
301
302
303def load_map_dialog(parent, filename, format='ccp4'):
304    form = parent.load_form('load_map')
305    normalize_setting = 'normalize_' + format + '_maps'
306
307    form.input_object_name.setText(
308            pymol.importing.filename_to_objectname(filename))
309    form.input_normalize.setChecked(parent.cmd.get_setting_int(normalize_setting) > 0)
310
311    def get_command(*args):
312        command = 'set %s, %d\n' % (normalize_setting,
313                1 if form.input_normalize.isChecked() else 0)
314        command += 'load ' + filename
315        name = form.input_object_name.text()
316        if name:
317            command += ', \\\n    ' + name
318        else:
319            name = pymol.importing.filename_to_objectname(filename)
320
321        selesuffix = ''
322
323        level = round(form.input_level.value(), 4)
324        sele = form.input_selection.currentText()
325        if sele:
326            buf = form.input_buffer.value()
327            selesuffix += ', %s, %s' % (sele, buf)
328            if form.check_carve.isChecked():
329                selesuffix += ', carve=%s' % (buf)
330
331        if form.check_volume.isChecked():
332            name_volume = form.input_name_volume.text() or (name + '_volume')
333            command += '\nvolume %s, %s, %s blue .5 %s yellow 0' % (
334                    name_volume, name, level, level * 2)
335            command += selesuffix
336
337        if form.check_isomesh.isChecked():
338            name_isomesh = form.input_name_isomesh.text() or (name + '_isomesh')
339            command += '\nisomesh %s, %s, %s' % (name_isomesh, name, level)
340            command += selesuffix
341
342        if form.check_isosurface.isChecked():
343            name_isosurface = form.input_name_isosurface.text() or (name + '_isosurface')
344            command += '\nisosurface %s, %s, %s' % (name_isosurface, name, level)
345            command += selesuffix
346
347        return command
348
349    def update_output_command(*args):
350        form.output_command.setText(get_command())
351
352    def run():
353        parent.cmd.do(get_command())
354        form._dialog.close()
355
356    # hook up events
357    form.input_normalize.stateChanged.connect(update_output_command)
358    form.input_object_name.textChanged.connect(update_output_command)
359    form.check_volume.stateChanged.connect(update_output_command)
360    form.check_isomesh.stateChanged.connect(update_output_command)
361    form.check_isosurface.stateChanged.connect(update_output_command)
362    form.check_carve.stateChanged.connect(update_output_command)
363    form.input_name_volume.textChanged.connect(update_output_command)
364    form.input_name_isomesh.textChanged.connect(update_output_command)
365    form.input_name_isosurface.textChanged.connect(update_output_command)
366    form.input_selection.editTextChanged.connect(update_output_command)
367    form.input_level.valueChanged.connect(update_output_command)
368    form.input_buffer.valueChanged.connect(update_output_command)
369    form.button_ok.clicked.connect(run)
370
371    update_output_command()
372    form._dialog.setModal(True)
373    form._dialog.show()
374
375
376def _get_assemblies(pdbid):
377    # TODO move to another module
378    import json
379    pdbid = pdbid.lower()
380    url = "https://www.ebi.ac.uk/pdbe/api/pdb/entry/summary/" + pdbid
381    try:
382        data = json.load(urllib.urlopen(url))
383        assembies = data[pdbid][0]['assemblies']
384        return [a['assembly_id'] for a in assembies]
385    except LookupError:
386        pass
387    except Exception as e:
388        print('_get_assemblies failed')
389        print(e)
390    return []
391
392
393def _get_chains(pdbid):
394    # TODO move to another module
395    url = "http://www.rcsb.org/pdb/rest/describeMol?structureId=" + pdbid
396    try:
397        from lxml import etree
398    except ImportError:
399        from xml.etree import ElementTree as etree
400    try:
401        data = etree.parse(urllib.urlopen(url))
402        return [e.get('id') for e in data.findall('./*/polymer/chain')]
403    except Exception as e:
404        print('_get_chains failed')
405        print(e)
406    return []
407
408
409def file_fetch_pdb(parent):
410    form = parent.load_form('fetch')
411    form.input_assembly.setEditText(parent.cmd.get('assembly'))
412
413    def get_command(*args):
414        code = form.input_code.text()
415        if len(code) != 4:
416            return ''
417
418        def get_name(w):
419            name = w.text()
420            if name:
421                return ', ' + name
422            return ''
423
424        if form.input_check_pdb.isChecked():
425            command = 'set assembly, "%s"\nfetch %s%s%s' % (
426                    form.input_assembly.currentText(),
427                    code, form.input_chain.currentText(),
428                    get_name(form.input_name))
429        else:
430            command = ''
431
432        if form.input_check_2fofc.isChecked():
433            command += '\nfetch %s%s, type=2fofc' % (
434                    code, get_name(form.input_name_2fofc))
435
436        if form.input_check_fofc.isChecked():
437            command += '\nfetch %s%s, type=fofc' % (
438                    code, get_name(form.input_name_fofc))
439
440        return command
441
442    def update_output_command(*args):
443        form.output_command.setText(get_command())
444
445    def code_changed(code):
446        for combo in [form.input_assembly, form.input_chain]:
447            if combo.count() != 1:
448                text = combo.currentText()
449                combo.clear()
450                combo.addItem('')
451                combo.setEditText(text)
452        if len(code) == 4:
453            update_assemblies(code)
454            update_chains(code)
455        update_output_command()
456
457    def run():
458        if len(form.input_code.text()) != 4:
459            QtWidgets.QMessageBox.warning(parent, "Error", "Need 4 letter PDB code")
460            return
461        parent.cmd.do(get_command())
462        form._dialog.close()
463
464    # async events
465    update_assemblies = AsyncFunc(_get_assemblies, form.input_assembly.addItems)
466    update_chains = AsyncFunc(_get_chains, form.input_chain.addItems)
467
468    # hook up events
469    form.input_code.textChanged.connect(code_changed)
470    form.input_chain.editTextChanged.connect(update_output_command)
471    form.input_assembly.editTextChanged.connect(update_output_command)
472    form.input_name.textChanged.connect(update_output_command)
473    form.input_name_2fofc.textChanged.connect(update_output_command)
474    form.input_name_fofc.textChanged.connect(update_output_command)
475    form.input_check_pdb.stateChanged.connect(update_output_command)
476    form.input_check_2fofc.stateChanged.connect(update_output_command)
477    form.input_check_fofc.stateChanged.connect(update_output_command)
478    form.button_ok.clicked.connect(run)
479
480    update_output_command()
481    form._dialog.show()
482
483
484def file_save(parent):
485    form = parent.load_form('save_molecule')
486    default_selection = form.input_selection.currentText()
487
488    get_setting_int = parent.cmd.get_setting_int
489    form.input_no_pdb_conect_nodup.setChecked(  not get_setting_int('pdb_conect_nodup'))
490    form.input_pdb_conect_all.setChecked(           get_setting_int('pdb_conect_all'))
491    form.input_no_ignore_pdb_segi.setChecked(   not get_setting_int('ignore_pdb_segi'))
492    form.input_pdb_retain_ids.setChecked(           get_setting_int('pdb_retain_ids'))
493    form.input_retain_order.setChecked(             get_setting_int('retain_order'))
494
495    models = parent.cmd.get_object_list()
496    selections = parent.cmd.get_names('public_selections')
497    names = models + selections
498
499    form.input_state.addItems(map(str, range(1, parent.cmd.count_states() + 1)))
500
501    form.input_selection.addItems(names)
502    form.input_selection.lineEdit().setPlaceholderText(default_selection)
503
504    formats = [
505        'PDBx/mmCIF (*.cif *.cif.gz)',
506        'PDB (*.pdb *.pdb.gz)',
507        'PQR (*.pqr)',
508        'MOL2 (*.mol2)',
509        'MDL SD (*.sdf *.mol)',
510        'Maestro (*.mae)',
511        'MacroModel (*.mmd *.mmod *.dat)',
512        'ChemPy Pickle (*.pkl)',
513        'XYZ (*.xyz)',
514        'MMTF (*.mmtf)',
515        'By Extension (*.*)',
516    ]
517
518    @PopupOnException.decorator
519    def run(*_):
520        selection = form.input_selection.currentText() or default_selection
521        state = int(form.input_state.currentText().split()[0])
522
523        parent.cmd.set('pdb_conect_nodup',    not form.input_no_pdb_conect_nodup.isChecked())
524        parent.cmd.set('pdb_conect_all',          form.input_pdb_conect_all.isChecked())
525        parent.cmd.set('ignore_pdb_segi',     not form.input_no_ignore_pdb_segi.isChecked())
526        parent.cmd.set('pdb_retain_ids',          form.input_pdb_retain_ids.isChecked())
527        parent.cmd.set('retain_order',            form.input_retain_order.isChecked())
528
529        if form.input_multi_state.isChecked():
530            fmt = form.input_multi_state_fmt.text()
531        elif form.input_multi_object.isChecked():
532            fmt = form.input_multi_object_fmt.text()
533        else:
534            fmt = ''
535
536        if fmt and form.input_multi_prompt.isChecked():
537            fss = parent.cmd.multifilenamegen(fmt, selection, state)
538        else:
539            fss = [(fmt, selection, state)]
540
541        for fname, selection, state in fss:
542            fname = getSaveFileNameWithExt(parent,
543                'Save Molecule As...',
544                os.path.join(parent.initialdir, fname),
545                filter=';;'.join(formats))
546
547            if not fname:
548                return
549
550            parent.initialdir = os.path.dirname(fname)
551
552            if form.input_multisave.isChecked():
553                parent.cmd.multisave(fname, selection, state, quiet=0)
554            elif '{' in os.path.basename(fname):
555                parent.cmd.multifilesave(fname, selection, state, quiet=0)
556            else:
557                parent.cmd.save(fname, selection, state, quiet=0)
558                parent.recent_filenames_add(fname)
559
560        form._dialog.close()
561
562    form.input_multi_state.pressed.connect(
563        lambda: form.input_state.setCurrentIndex(1))
564
565    form.button_ok.clicked.connect(run)
566    form._dialog.show()
567
568
569def file_save_png(parent):  #noqa
570    if parent.dialog_png is not None:
571        parent.dialog_png.show()
572        return
573
574    form = parent.load_form('png')
575    parent.dialog_png = form._dialog
576
577    def run():
578        from pymol import exporting
579        fname = getSaveFileNameWithExt(parent, 'Save As...', parent.initialdir,
580                                filter='PNG File (*.png)')
581        if not fname:
582            return
583
584        parent.initialdir = os.path.dirname(fname)
585
586        rendering = form.input_rendering.currentIndex()
587        ray = 0
588        width, height, dpi = 0, 0, -1
589
590        '''
591        dpi = float(form.input_dpi.currentText())
592
593        width = exporting._unit2px(
594            form.input_width.value(), dpi,
595            form.input_width_unit.currentText())
596        height = exporting._unit2px(
597            form.input_height.value(), dpi,
598            form.input_height_unit.currentText())
599        '''
600
601        form._dialog.hide()
602
603        if rendering == 1:
604            parent.cmd.do('draw %d, %d' % (width, height))
605            width = 0
606            height = 0
607        elif rendering == 2:
608            parent.cmd.do('set opaque_background, 1')
609            ray = 1
610        elif rendering == 3:
611            parent.cmd.do('set opaque_background, 0')
612            ray = 1
613
614        parent.cmd.sync()
615        parent.cmd.do(
616            'png %s, %d, %d, %d, ray=%d' %
617            (fname, width, height, dpi, ray))
618
619    '''
620    def units_changed():
621        dpi = float(form.input_dpi.currentText())
622        width_unit = form.input_width_unit.currentText()
623        height_unit = form.input_width_unit.currentText()
624        if dpi < 1 and (width_unit != 'px' or height_unit != 'px'):
625            form.input_dpi.setCurrentIndex(1)
626            dpi = float(form.input_dpi.currentText())
627
628    # initial values
629    form.input_width.setValue(pymol.cmd.get_viewport()[0])
630
631    dpi_index = 0
632    dpi_values = [-1, 150, 300]
633    dpi = pymol.cmd.get_setting_int('image_dots_per_inch')
634
635    if dpi > 0:
636        try:
637            dpi_index = dpi_values.index(dpi)
638        except ValueError:
639            dpi_values.append(dpi)
640            dpi_values.sort()
641            dpi_index = dpi_values.index(dpi)
642
643    for dpi in dpi_values:
644        form.input_dpi.addItem(str(dpi))
645    form.input_dpi.setCurrentIndex(dpi_index)
646
647    # hook up events
648    form.input_width_unit.currentIndexChanged.connect(units_changed)
649    form.input_height_unit.currentIndexChanged.connect(units_changed)
650    '''
651    form.button_ok.clicked.connect(run)
652
653    form._dialog.show()
654
655
656def file_save_mpeg(parent, _preselect=None):
657    form = parent.load_form('movieexport')
658
659    filters = {
660        'png': 'Numbered PNG Files (*.png)',
661        'mp4': 'MPEG 4 movie file (*.mp4)',
662        'mpg': 'MPEG 1 movie file (*.mpg *.mpeg)',
663        'mov': 'QuickTime (*.mov)',
664        'gif': 'Animated GIF (*.gif)',
665    }
666
667    support = {
668        '':             {'mp4': 0, 'mpg': 0, 'mov': 0, 'gif': 0},
669        'ffmpeg':       {'mp4': 1, 'mpg': 1, 'mov': 1, 'gif': 1},
670        'mpeg_encode':  {'mp4': 0, 'mpg': 1, 'mov': 0, 'gif': 0},
671        'convert':      {'mp4': 0, 'mpg': 0, 'mov': 0, 'gif': 1},
672    }
673
674    from pymol.movie import find_exe as has_exe
675
676    def update_encoder_options():
677        encoder = form.input_encoder.currentText()
678
679        for fmt, enabled in support[encoder].items():
680            w = getattr(form, 'format_' + fmt)
681            w.setEnabled(enabled)
682            if not enabled and w.isChecked():
683                if encoder == 'mpeg_encode':
684                    form.format_mpg.setChecked(True)
685                elif encoder == 'convert':
686                    form.format_gif.setChecked(True)
687                else:
688                    form.format_png.setChecked(True)
689
690        form.input_quality.setEnabled(encoder not in ("", "convert"))
691
692        if encoder and not has_exe(encoder):
693            msg = "Encoder '%s' is not installed." % encoder
694            pkg = None
695            url = None
696            if not pkg:
697                QtWidgets.QMessageBox.warning(parent, "Warning", msg)
698            else:
699                from pymol.Qt import utils
700                utils.conda_ask_install(pkg[1], pkg[0], msg, url=url)
701
702    if _preselect == 'png':
703        form.format_png.setChecked(True)
704        form.group_format.hide()
705    else:
706        for i in range(1, form.input_encoder.count()):
707            encoder = form.input_encoder.itemText(i)
708            if has_exe(encoder):
709                form.input_encoder.setCurrentIndex(i)
710                break
711
712        if _preselect == 'mov':
713            encoder = 'ffmpeg'
714            form.input_encoder.setCurrentIndex(1)
715            form.format_mov.setChecked(True)
716        elif encoder == 'ffmpeg':
717            form.format_mp4.setChecked(True)
718        else:
719            form.format_mpg.setChecked(True)
720
721    form._dialog.adjustSize()
722
723    @PopupOnException.decorator
724    def run(*_):
725        for fmt in filters:
726            w = getattr(form, 'format_' + fmt)
727            if w.isChecked():
728                break
729        fname = getSaveFileNameWithExt(parent,
730                'Save As...', parent.initialdir,
731                filter=filters[fmt])
732        if not fname:
733            return
734
735        parent.initialdir = os.path.dirname(fname)
736
737        if fmt == 'png':
738            parent.cmd.mpng(fname,
739                    width=form.input_width.value(),
740                    height=form.input_height.value(),
741                    mode=2 if form.input_ray.isChecked() else 1,
742                    quiet=0, modal=-1)
743        else:
744            mode = 'ray' if form.input_ray.isChecked() else 'draw'
745            encoder = form.input_encoder.currentText()
746
747            parent.cmd.movie.produce(fname,
748                    width=form.input_width.value(),
749                    height=form.input_height.value(),
750                    quality=form.input_quality.value(),
751                    mode=mode, encoder=encoder,
752                    quiet=0)
753
754    def set_resolution(height):
755        w, h = form.input_width.value(), max(1, form.input_height.value())
756        aspect = (w / h) if (w and h) else 9999
757        width = int(round(min(aspect, 16. / 9.) * height / 2.)) * 2
758        form.input_width.setValue(width)
759        form.input_height.setValue(height)
760
761    # initial values
762    viewport = parent.cmd.get_viewport()
763    form.input_width.setValue(viewport[0])
764    form.input_height.setValue(viewport[1])
765    form.input_quality.setValue(parent.cmd.get_setting_int('movie_quality'))
766    if parent.cmd.get_setting_int('ray_trace_frames'):
767        form.input_ray.setChecked(True)
768    update_encoder_options()
769
770    # hook up events
771    form.button_ok.clicked.connect(run)
772    form.input_encoder.currentIndexChanged.connect(update_encoder_options)
773
774    for height in (720, 480, 360):
775        button = getattr(form, 'button_%dp' % height)
776        button.pressed.connect(lambda h=height: set_resolution(h))
777
778    form._dialog.show()
779
780
781def _file_save_object(self, otype, formats, noobjectsmsg):
782    names = self.cmd.get_names_of_type(otype)
783
784    if not names:
785        QtWidgets.QMessageBox.warning(self, "Warning", noobjectsmsg)
786        return
787
788    form = self.load_form('save_object')
789    form.input_name.addItems(names)
790    form._dialog.setWindowTitle('Save ' + otype)
791
792    def run():
793        name = form.input_name.currentText()
794
795        fname = getSaveFileNameWithExt(self,
796            'Save As...',
797            self.initialdir,
798            filter=';;'.join(formats))
799
800        if not fname:
801            return
802
803        self.cmd.save(fname, name, -1, quiet=0)
804        form._dialog.close()
805
806    form.button_ok.clicked.connect(run)
807    form._dialog.show()
808
809
810def file_save_map(self):
811    return _file_save_object(self, 'object:map', ['CCP4 (*.ccp4 *.map)'],
812            'No map objects loaded')
813
814
815def file_save_aln(self):
816    url = "http://pymolwiki.org/index.php/Align#Alignment_Objects"
817    return _file_save_object(self, 'object:alignment', ['clustalw (*.aln)'],
818            'No alignment objects loaded\n\n'
819            'Hint: create alignment objects with "align" and\n'
820            '"super" using the "object=..." argument.')
821