def __init__(self, session, tool_name, *, title=None): ToolInstance.__init__(self, session, tool_name) from chimerax.ui import MainToolWindow tw = MainToolWindow(self) if title is not None: tw.title = title self.tool_window = tw parent = tw.ui_area from matplotlib import figure self.figure = f = figure.Figure(dpi=100, figsize=(2, 2)) from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as Canvas self.canvas = c = Canvas(f) parent.setMinimumHeight( 1 ) # Matplotlib gives divide by zero error when plot resized to 0 height. c.setParent(parent) from PyQt5.QtWidgets import QHBoxLayout layout = QHBoxLayout() layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(c) parent.setLayout(layout) tw.manage(placement="side") self.axes = axes = f.gca() self._pan = None # Pan/zoom mouse control
def __init__(self, session, tool_name): self._requested_halt = False self._model_move_handler = None self._last_relative_position = None self.max_steps = 2000 self.ijk_step_size_min = 0.01 self.ijk_step_size_max = 0.5 self._last_status_time = 0 self._status_interval = 0.5 # seconds ToolInstance.__init__(self, session, tool_name) from chimerax.ui import MainToolWindow tw = MainToolWindow(self) self.tool_window = tw parent = tw.ui_area from PyQt5.QtWidgets import QVBoxLayout, QLabel layout = QVBoxLayout(parent) layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) parent.setLayout(layout) # Make menus to choose molecule and map for fitting mf = self._create_mol_map_menu(parent) layout.addWidget(mf) # Report correlation from chimerax.ui.widgets import EntriesRow cr = EntriesRow( parent, 'Correlation', 0.0, 'Average map value', 0.0, ('Update', lambda *args, self=self: self._update_metric_values(log=True))) self._corr_label, self._ave_label = cl, al = cr.values cl.value = al.value = None # Make fields blank # Options panel options = self._create_options_gui(parent) layout.addWidget(options) # Fit, Undo, Redo buttons bf = self._create_action_buttons(parent) layout.addWidget(bf) # Status line self._status_label = sl = QLabel(parent) layout.addWidget(sl) layout.addStretch(1) # Extra space at end tw.manage(placement="side")
def create_button_panel(self): from chimerax.ui import MainToolWindow tw = MainToolWindow(self, close_destroys=False) self.tool_window = tw p = tw.ui_area from PyQt5.QtWidgets import QVBoxLayout layout = QVBoxLayout(p) layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) b = self.create_panel_buttons(p) p.setLayout(layout) layout.addWidget(b) tw.manage(placement="side")
class FilePanel(ToolInstance): SESSION_ENDURING = True help = "help:user/tools/filehistory.html" def __init__(self, session, tool_name): ToolInstance.__init__(self, session, tool_name) from chimerax.ui import MainToolWindow self.tool_window = MainToolWindow(self, close_destroys=False) parent = self.tool_window.ui_area from chimerax.ui.file_history import FileHistory fh = FileHistory(session, parent, size_hint=(575, 200)) self.tool_window.manage(placement="side")
def __init__(self, session, title): ToolInstance.__init__(self, session, title) self.title = title self._rows = None self._columns = None self._next_row_col = (1, 1) self._fill_order = 'rows' self._buttons = {} # Map (row,column) -> QPushButton from chimerax.ui import MainToolWindow tw = MainToolWindow(self) self.tool_window = tw from PyQt5.QtWidgets import QGridLayout self._layout = layout = QGridLayout() layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) tw.ui_area.setLayout(layout) tw.manage(placement="side")
def __init__(self, session, tool_name): ToolInstance.__init__(self, session, tool_name) self.minimize_steps = 10 self.edge_thickness = 0.1 # Edge diameter as fraction of edge length. self.display_name = 'Cage Builder' from chimerax.ui import MainToolWindow tw = MainToolWindow(self) self.tool_window = tw parent = tw.ui_area from PyQt5.QtWidgets import QHBoxLayout, QLabel, QPushButton, QLineEdit layout = QHBoxLayout() layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) cp = QLabel('Create polygon') layout.addWidget(cp) b5 = QPushButton('5') b5.clicked.connect(lambda e: self.attach_polygons(5)) layout.addWidget(b5) b6 = QPushButton('6') b6.clicked.connect(lambda e: self.attach_polygons(6)) layout.addWidget(b6) mn = QPushButton('Minimize') mn.clicked.connect(self.minimize_cb) layout.addWidget(mn) ell = QLabel(' Edge length') layout.addWidget(ell) self.edge_length = el = QLineEdit('50') el.setMaximumWidth(30) layout.addWidget(el) dp = QPushButton('Delete') dp.clicked.connect(self.delete_polygon_cb) layout.addWidget(dp) layout.addStretch(1) # Extra space at end of button row. parent.setLayout(layout) tw.manage(placement="side")
def __init__(self, session, tool_name): ToolInstance.__init__(self, session, tool_name) from chimerax.ui import MainToolWindow tw = MainToolWindow(self) self.tool_window = tw parent = tw.ui_area from PyQt5.QtWidgets import QVBoxLayout, QLabel layout = QVBoxLayout(parent) layout.setContentsMargins(0,0,0,0) layout.setSpacing(0) parent.setLayout(layout) # Heading heading = QLabel('Placement of data array in x,y,z coordinate space:', parent) layout.addWidget(heading) # Make menus to choose molecule and map for fitting mf = self._create_map_menu(parent) layout.addWidget(mf) # GUI for origin, step, angles, axis settings. options = self._create_parameters_gui(parent) layout.addWidget(options) # Apply button # bf = self._create_action_buttons(parent) # layout.addWidget(bf) # Status line self._status_label = sl = QLabel(parent) layout.addWidget(sl) layout.addStretch(1) # Extra space at end self._update_gui_values() tw.manage(placement="side")
class ShellUI(ToolInstance): # shell tool does not participate in sessions SESSION_ENDURING = True def __init__(self, session, tool_name): ToolInstance.__init__(self, session, tool_name) # 'display_name' defaults to class name with spaces inserted # between lower-then-upper-case characters (therefore "Tool UI" # in this case), so only override if different name desired self.display_name = "ChimeraX Python Shell" from chimerax.ui import MainToolWindow self.tool_window = MainToolWindow(self) parent = self.tool_window.ui_area # UI content code from ipykernel.ipkernel import IPythonKernel save_ns = IPythonKernel.user_ns IPythonKernel.user_ns = {'session': session} from qtconsole.inprocess import QtInProcessKernelManager kernel_manager = QtInProcessKernelManager() kernel_manager.start_kernel() kernel_client = kernel_manager.client() kernel_client.start_channels() from qtconsole.rich_jupyter_widget import RichJupyterWidget self.shell = RichJupyterWidget(parent) def_banner = self.shell.banner self.shell.banner = "{}\nCurrent ChimeraX session available as 'session'.\n\n".format( def_banner) self.shell.kernel_manager = kernel_manager self.shell.kernel_client = kernel_client IPythonKernel.user_ns = save_ns from PyQt5.QtWidgets import QHBoxLayout layout = QHBoxLayout() layout.addWidget(self.shell) layout.setStretchFactor(self.shell, 1) parent.setLayout(layout) self.tool_window.manage(placement=None)
class SideViewUI(ToolInstance): help = "help:user/tools/sideview.html" def __init__(self, session, tool_name): ToolInstance.__init__(self, session, tool_name) from chimerax.ui import MainToolWindow self.tool_window = MainToolWindow(self) parent = self.tool_window.ui_area # UI content code from PyQt5.QtCore import Qt from PyQt5.QtWidgets import QLabel, QHBoxLayout, QVBoxLayout, QCheckBox, QStackedWidget self.view = v = View(session.models.scene_root_model, window_size=(0, 0)) v.initialize_rendering(session.main_view.render.opengl_context) # TODO: from chimerax.graphics.camera import OrthographicCamera v.camera = OrthoCamera() if self.display_name.startswith('Top'): side = SideViewCanvas.TOP_SIDE else: side = SideViewCanvas.RIGHT_SIDE self.opengl_canvas = SideViewCanvas(parent, v, session, self, side=side) clip = QLabel(parent) clip.setText("clip:") self.clip_near = QCheckBox(parent) self.clip_near.setText("near") self.clip_near.down = False # TODO: parent.Bind(wx.EVT_CHECKBOX, self.on_near, self.clip_near) self.clip_near.clicked.connect(self.on_near) self.clip_far = QCheckBox(parent) self.clip_far.setText("far") self.clip_far.down = False # TODO: parent.Bind(wx.EVT_CHECKBOX, self.on_far, self.clip_far) self.clip_far.clicked.connect(self.on_far) button_layout = QHBoxLayout() button_layout.addWidget(clip, alignment=Qt.AlignCenter) button_layout.addWidget(self.clip_near) button_layout.addWidget(self.clip_far) button_layout.addStretch(1) class graphics_area(QStackedWidget): def sizeHint(self): # noqa from PyQt5.QtCore import QSize return QSize(200, 200) layout = QVBoxLayout() ga = graphics_area(parent) ga.addWidget(self.opengl_canvas.widget) layout.addWidget(ga, 1) layout.addLayout(button_layout) parent.setLayout(layout) self.tool_window.manage(placement="side") def delete(self): self.opengl_canvas.close() self.view.delete() self.view = None ToolInstance.delete(self) def on_near(self, event): session = self._session() v = session.main_view planes = v.clip_planes if not self.clip_near.isChecked(): planes.remove_plane('near') return p = planes.find_plane('near') if p: return b = v.drawing_bounds() if b is None: session.logger.info("Can not turn on clipping since there are no models to clip") self.clip_near.setChecked(False) return near, far = v.near_far_distances(v.camera, None) camera_pos = v.camera.position.origin() vd = v.camera.view_direction() plane_point = camera_pos + near * vd planes.set_clip_position('near', plane_point, v) def on_far(self, event): session = self._session() v = session.main_view planes = v.clip_planes if not self.clip_far.isChecked(): planes.remove_plane('far') return p = planes.find_plane('far') if p: return b = v.drawing_bounds() if b is None: session.logger.info("Can not turn on clipping since there are no models to clip") self.clip_far.setChecked(False) return near, far = v.near_far_distances(v.camera, None) camera_pos = v.camera.position.origin() vd = v.camera.view_direction() plane_point = camera_pos + far * vd planes.set_clip_position('far', plane_point, v)
class ToolshedUI(ToolInstance): SESSION_ENDURING = True def __init__(self, session, tool_name): # Standard template stuff ToolInstance.__init__(self, session, tool_name) self.display_name = "Toolshed" from chimerax.ui import MainToolWindow self.tool_window = MainToolWindow(self) self.tool_window.manage(placement=None) parent = self.tool_window.ui_area from PyQt5.QtWidgets import QGridLayout, QTabWidget from chimerax.ui.widgets import HtmlView layout = QGridLayout() layout.setContentsMargins(0, 0, 0, 0) self.tab_widget = QTabWidget() self.html_view = HtmlView(size_hint=(1000, 600), download=self._download) self.tab_widget.addTab(self.html_view, "Toolshed") layout.addWidget(self.tab_widget, 0, 0) parent.setLayout(layout) from PyQt5.QtCore import QUrl self.html_view.setUrl(QUrl(self.url)) self._pending_downloads = [] @property def url(self): from urllib.parse import urlencode from distutils.util import get_platform from chimerax.core import buildinfo params = urlencode({"platform":get_platform(), "version":buildinfo.version}) url = "%s?%s" % (self.session.toolshed.remote_url, params) return url def _intercept(self, info): # "info" is an instance of QWebEngineUrlRequestInfo qurl = info.requestUrl() # print("intercept", qurl.toString()) def _download(self, item): # "item" is an instance of QWebEngineDownloadItem # print("ToolshedUI._download", item) import os.path, os urlFile = item.url().fileName() base, extension = os.path.splitext(urlFile) item.finished.connect(self._download_finished) # print("ToolshedUI._download connect", item.mimeType(), extension) # Normally, we would look at the download type or MIME type, # but since neither one is set by the server, we look at the # download extension instead if extension == ".whl": # Since the file name encodes the package name and version # number, we make sure that we are using the right name # instead of whatever QWebEngine may want to use. # Remove _# which may be present if bundle author submitted # the same version of the bundle multiple times. parts = base.rsplit('_', 1) if len(parts) == 2 and parts[1].isdigit(): urlFile = parts[0] + extension filePath = os.path.join(os.path.dirname(item.path()), urlFile) item.setPath(filePath) # print("ToolshedUI._download clean") try: # Guarantee that file name is available os.remove(filePath) except OSError: pass self._pending_downloads.append(item) self.session.logger.info("Downloading bundle %s" % urlFile) # print("ToolshedUI._download accept") item.accept() else: # print("ToolshedUI._download cancel") item.cancel() def _download_finished(self, *args, **kw): # print("ToolshedUI._download_finished", args, kw) finished = [] pending = [] for item in self._pending_downloads: if not item.isFinished(): pending.append(item) else: finished.append(item) self._pending_downloads = pending install_cmd = ["install", "--quiet"] need_reload = False for item in finished: item.finished.disconnect() filename = item.path() from chimerax.ui.ask import ask how = ask(self.session, "Install %s for:" % filename, ["all users", "just me", "cancel"], title="Toolshed") if how == "cancel": self.session.logger.info("Bundle installation canceled") continue elif how == "just me": per_user = True else: per_user = False self.session.toolshed.install_bundle(filename, self.session.logger, per_user=per_user, session=self.session) def _navigate(self, qurl): session = self.session # Handle event # data is QUrl url = qurl.toString() def link_handled(): return False if url.startswith("toolshed:"): link_handled() parts = url.split(':') method = getattr(self, parts[1]) args = parts[2:] method(session, *args) def _make_page(self, *args): session = self.session ts = session.toolshed tools = session.tools from io import StringIO page = _PageTemplate # TODO: handle multiple versions of available tools # TODO: add "update" link for installed tools # running def tool_key(t): return t.display_name s = StringIO() tool_list = tools.list() if not tool_list: print('<p class="empty">No running tools found.</p>', file=s) else: print("<table>", file=s) for t in sorted(tool_list, key=tool_key): show_link = _SHOW_LINK % t.id hide_link = _HIDE_LINK % t.id kill_link = _KILL_LINK % t.id links = " ".join([show_link, hide_link, kill_link]) print(_RUNNING_ROW % (links, t.display_name), file=s) print("</table>", file=s) page = page.replace("RUNNING_TOOLS", s.getvalue()) # installed def bundle_key(bi): return bi.name s = StringIO() bi_list = ts.bundle_info(session.logger, installed=True, available=False) if not bi_list: print('<p class="empty">No installed tools found.</p>', file=s) else: print("<table>", file=s) for bi in sorted(bi_list, key=bundle_key): start_link = _START_LINK % bi.name update_link = _UPDATE_LINK % bi.name remove_link = _REMOVE_LINK % bi.name links = " ".join([start_link, update_link, remove_link]) print(_ROW % (links, bi.name, bi.synopsis), file=s) print("</table>", file=s) page = page.replace("INSTALLED_TOOLS", s.getvalue()) installed_bundles = dict([(bi.name, bi) for bi in bi_list]) # available s = StringIO() bi_list = ts.bundle_info(installed=False, available=True) if not bi_list: print('<p class="empty">No available tools found.</p>', file=s) else: any_shown = False for bi in sorted(bi_list, key=bundle_key): try: # If this bundle is already installed, do not display it installed = installed_bundles[bi.name] if installed.version == bi.version: continue except KeyError: pass if not any_shown: print("<table>", file=s) any_shown = True link = _INSTALL_LINK % bi.name print(_ROW % (link, bi.name, bi.synopsis), file=s) if any_shown: print("</table>", file=s) else: print('<p class="empty">All available tools are installed.</p>', file=s) page = page.replace("AVAILABLE_TOOLS", s.getvalue()) page = page.replace("TOOLSHED_URL", ts.remote_url) self.webview.history().clear() self.webview.setHtml(page) def refresh_installed(self, session): # refresh list of installed tools from . import cmd cmd.ts_refresh(session, bundle_type="installed") self._make_page() def refresh_available(self, session): # refresh list of available tools from . import cmd cmd.ts_refresh(session, bundle_type="available") self._make_page() def _start_tool(self, session, tool_name): # start installed tool from . import cmd cmd.ts_start(session, tool_name) self._make_page() def _update_tool(self, session, tool_name): # update installed tool from . import cmd cmd.ts_update(session, tool_name) self._make_page() def _remove_tool(self, session, tool_name): # remove installed tool from . import cmd cmd.ts_remove(session, tool_name) self._make_page() def _install_tool(self, session, tool_name): # install available tool from . import cmd cmd.ts_install(session, tool_name) self._make_page() def _find_tool(self, session, tool_id): t = session.tools.find_by_id(int(tool_id)) if t is None: raise RuntimeError("cannot find tool with id \"%s\"" % tool_id) return t def _show_tool(self, session, tool_id): self._find_tool(session, tool_id).display(True) self._make_page() def _hide_tool(self, session, tool_id): self._find_tool(session, tool_id).display(False) self._make_page() def _kill_tool(self, session, tool_id): self._find_tool(session, tool_id).delete() self._make_page() def button_test(self, session, *args): session.logger.info("ToolshedUI.button_test: %s" % str(args)) @classmethod def get_singleton(cls, session): from chimerax.core import tools return tools.get_singleton(session, ToolshedUI, 'Toolshed') # # Override ToolInstance methods # def delete(self): self.tool_window = None super().delete()
class ToolbarTool(ToolInstance): SESSION_ENDURING = True SESSION_SAVE = False PLACEMENT = "top" CUSTOM_SCHEME = "toolbar" help = "help:user/tools/toolbar.html" # Let ChimeraX know about our help page def __init__(self, session, tool_name): super().__init__(session, tool_name) self.display_name = "Toolbar" global _settings if _settings is None: _settings = _ToolbarSettings(self.session, "Toolbar") from chimerax.ui import MainToolWindow self.tool_window = MainToolWindow(self, close_destroys=False, hide_title_bar=True) self._build_ui() self.tool_window.fill_context_menu = self.fill_context_menu session.triggers.add_handler('set right mouse', self._set_right_mouse_button) def _build_ui(self): from chimerax.ui.widgets.tabbedtoolbar import TabbedToolbar from PyQt5.QtWidgets import QVBoxLayout layout = QVBoxLayout() margins = layout.contentsMargins() margins.setTop(0) margins.setBottom(0) layout.setContentsMargins(margins) self.ttb = TabbedToolbar( self.tool_window.ui_area, show_section_titles=_settings.show_section_labels, show_button_titles=_settings.show_button_labels) layout.addWidget(self.ttb) self._build_tabs() self.tool_window.ui_area.setLayout(layout) self.tool_window.manage(self.PLACEMENT) def fill_context_menu(self, menu, x, y): # avoid having actions destroyed when this routine returns # by stowing a reference in the menu itself from PyQt5.QtWidgets import QAction button_labels = QAction("Show button labels", menu) button_labels.setCheckable(True) button_labels.setChecked(_settings.show_button_labels) button_labels.toggled.connect(lambda arg, f=self._set_button_labels: f(arg)) menu.addAction(button_labels) section_labels = QAction("Show section labels", menu) section_labels.setCheckable(True) section_labels.setChecked(_settings.show_section_labels) section_labels.toggled.connect(lambda arg, f=self._set_section_labels: f(arg)) menu.addAction(section_labels) settings_action = QAction("Settings...", menu) settings_action.triggered.connect(lambda arg: self.show_settings()) menu.addAction(settings_action) def show_settings(self): if not hasattr(self, "settings_tool"): self.settings_tool = ToolbarSettingsTool( self.session, self, self.tool_window.create_child_window("Toolbar Settings", close_destroys=False)) self.settings_tool.tool_window.manage(None) self.settings_tool.tool_window.shown = True def _set_button_labels(self, show_button_labels): _settings.show_button_labels = show_button_labels self.ttb.set_show_button_titles(show_button_labels) def _set_section_labels(self, show_section_labels): _settings.show_section_labels = show_section_labels self.ttb.set_show_section_titles(show_section_labels) def build_home_tab(self): # (re)Build Home tab from settings from PyQt5.QtGui import QIcon self.ttb.clear_tab("Home") last_section = None for (section, compact, display_name, icon_path, description, link, bundle_info, name, kw) in _home_layout(self.session, _settings.home_tab): if section != last_section: last_section = section if compact: self.ttb.set_section_compact("Home", section, True) if icon_path is None: icon = None else: icon = QIcon(icon_path) def callback(event, session=self.session, name=name, bundle_info=bundle_info, display_name=display_name): bundle_info.run_provider(session, name, session.toolbar, display_name=display_name) self.ttb.add_button( "Home", section, display_name, callback, icon, description, **kw) def _build_tabs(self): # add buttons from toolbar manager from PyQt5.QtGui import QIcon from .manager import fake_mouse_mode_bundle_info self.right_mouse_buttons = {} self.current_right_mouse_button = None self.build_home_tab() # Build other tabs from toolbar manager toolbar = self.session.toolbar._toolbar last_tab = None last_section = None for (tab, section, compact, display_name, icon_path, description, bundle_info, name, kw) in _other_layout(self.session, toolbar): if tab != last_tab: last_tab = tab last_section = None if section != last_section: last_section = section if compact: self.ttb.set_section_compact(tab, section, True) if bundle_info == fake_mouse_mode_bundle_info: kw["vr_mode"] = name # Allows VR to recognize mouse mode buttons rmbs = self.right_mouse_buttons.setdefault(name, []) if icon_path is None: m = self.session.ui.mouse_modes.named_mode(name) if m is not None: icon_path = m.icon_path rmbs.append((tab, section, display_name, icon_path)) if icon_path is None: icon = None else: icon = QIcon(icon_path) def callback(event, session=self.session, name=name, bundle_info=bundle_info, display_name=display_name): bundle_info.run_provider(session, name, session.toolbar, display_name=display_name) self.ttb.add_button( tab, section, display_name, callback, icon, description, **kw) self.ttb.show_tab('Home') self._set_right_mouse_button('init', self.session.ui.mouse_modes.mode("right", exact=True)) def _set_right_mouse_button(self, trigger_name, mode): # highlight current right mouse button name = mode.name if mode is not None else None if name == self.current_right_mouse_button: return set_sections = set() has_button = name in self.right_mouse_buttons if has_button: for info in self.right_mouse_buttons[name]: tab_title, section_title, _, _ = info set_sections.add((tab_title, section_title)) if self.current_right_mouse_button is not None: # remove highlighting for info in self.right_mouse_buttons[self.current_right_mouse_button]: tab_title, section_title, button_title, icon_path = info redo = (tab_title, section_title) not in set_sections self.ttb.remove_button_highlight(tab_title, section_title, button_title, redo=redo) if not has_button: return # highlight button(s) self.current_right_mouse_button = name for info in self.right_mouse_buttons[name]: tab_title, section_title, button_title, icon_path = info self.ttb.add_button_highlight(tab_title, section_title, button_title)
class CommandLine(ToolInstance): SESSION_ENDURING = True show_history_label = "Command History..." compact_label = "Remove duplicate consecutive commands" help = "help:user/tools/cli.html" def __init__(self, session, tool_name): ToolInstance.__init__(self, session, tool_name) self._in_init = True from .settings import settings self.settings = settings from chimerax.ui import MainToolWindow self.tool_window = MainToolWindow(self, close_destroys=False, hide_title_bar=True) parent = self.tool_window.ui_area self.tool_window.fill_context_menu = self.fill_context_menu self.history_dialog = _HistoryDialog(self, self.settings.typed_only) from PyQt5.QtWidgets import QComboBox, QHBoxLayout, QLabel label = QLabel(parent) label.setText("Command:") class CmdText(QComboBox): def __init__(self, parent, tool): self.tool = tool QComboBox.__init__(self, parent) self._processing_key = False from PyQt5.QtCore import Qt # defer context menu to parent self.setContextMenuPolicy(Qt.NoContextMenu) self.setAcceptDrops(True) self._out_selection = None def dragEnterEvent(self, event): if event.mimeData().text(): event.acceptProposedAction() def dropEvent(self, event): text = event.mimeData().text() if text.startswith("file://"): text = text[7:] self.lineEdit().insert(text) event.acceptProposedAction() def focusInEvent(self, event): self._out_selection = None QComboBox.focusInEvent(self, event) def focusOutEvent(self, event): le = self.lineEdit() self._out_selection = (sel_start, sel_length, txt) = (le.selectionStart(), len(le.selectedText()), le.text()) QComboBox.focusOutEvent(self, event) if sel_start >= 0: le.setSelection(sel_start, sel_length) def keyPressEvent(self, event, forwarded=False): self._processing_key = True from PyQt5.QtCore import Qt from PyQt5.QtGui import QKeySequence if session.ui.key_intercepted(event.key()): return want_focus = forwarded and event.key() not in [ Qt.Key_Control, Qt.Key_Shift, Qt.Key_Meta, Qt.Key_Alt ] import sys control_key = Qt.MetaModifier if sys.platform == "darwin" else Qt.ControlModifier shifted = event.modifiers() & Qt.ShiftModifier if event.key() == Qt.Key_Up: # up arrow self.tool.history_dialog.up(shifted) elif event.key() == Qt.Key_Down: # down arrow self.tool.history_dialog.down(shifted) elif event.matches(QKeySequence.Undo): want_focus = False session.undo.undo() elif event.matches(QKeySequence.Redo): want_focus = False session.undo.redo() elif event.modifiers() & control_key: if event.key() == Qt.Key_N: self.tool.history_dialog.down(shifted) elif event.key() == Qt.Key_P: self.tool.history_dialog.up(shifted) elif event.key() == Qt.Key_U: self.tool.cmd_clear() self.tool.history_dialog.search_reset() elif event.key() == Qt.Key_K: self.tool.cmd_clear_to_end_of_line() self.tool.history_dialog.search_reset() else: QComboBox.keyPressEvent(self, event) else: QComboBox.keyPressEvent(self, event) if want_focus: # Give command line the focus, so that up/down arrow work as # expected rather than changing the selection level self.setFocus() self._processing_key = False def sizeHint(self): # prevent super-long commands from making the whole interface super wide return self.minimumSizeHint() self.text = CmdText(parent, self) self.text.setEditable(True) self.text.setCompleter(None) def sel_change_correction(): # don't allow selection to change while focus is out if self.text._out_selection is not None: start, length, text = self.text._out_selection le = self.text.lineEdit() if text != le.text(): self.text._out_selection = (le.selectionStart(), len(le.selectedText()), le.text()) return if start >= 0 and (start, length) != (le.selectionStart(), len(le.selectedText())): le.setSelection(start, length) self.text.lineEdit().selectionChanged.connect(sel_change_correction) # pastes can have a trailing newline, which is problematic when appending to the pasted command... def strip_trailing_newlines(): le = self.text.lineEdit() while le.text().endswith('\n'): le.setText(le.text()[:-1]) self.text.lineEdit().textEdited.connect(strip_trailing_newlines) self.text.lineEdit().textEdited.connect( self.history_dialog.search_reset) def text_change(*args): # if text changes while focus is out, remember new selection if self.text._out_selection is not None: le = self.text.lineEdit() self.text._out_selection = (le.selectionStart(), len(le.selectedText()), le.text()) self.text.lineEdit().selectionChanged.connect(text_change) layout = QHBoxLayout(parent) layout.setSpacing(1) layout.setContentsMargins(2, 0, 0, 0) layout.addWidget(label) layout.addWidget(self.text, 1) parent.setLayout(layout) # lineEdit() seems to be None during entire CmdText constructor, so connect here... self.text.lineEdit().returnPressed.connect(self.execute) self.text.currentTextChanged.connect(self.text_changed) self.text.forwarded_keystroke = lambda e: self.text.keyPressEvent( e, forwarded=True) session.ui.register_for_keystrokes(self.text) self.history_dialog.populate() self._just_typed_command = None self._command_started_handler = session.triggers.add_handler( "command started", self._command_started_cb) self.tool_window.manage(placement="bottom") self._in_init = False self._processing_command = False if self.settings.startup_commands: # prevent the startup command output from being summarized into 'startup messages' table session.ui.triggers.add_handler('ready', self._run_startup_commands) def cmd_clear(self): self.text.lineEdit().clear() def cmd_clear_to_end_of_line(self): le = self.text.lineEdit() t = le.text()[:le.cursorPosition()] le.setText(t) def cmd_replace(self, cmd): line_edit = self.text.lineEdit() line_edit.setText(cmd) line_edit.setCursorPosition(len(cmd)) def delete(self): self.session.ui.deregister_for_keystrokes(self.text) self.session.triggers.remove_handler(self._command_started_handler) super().delete() def fill_context_menu(self, menu, x, y): # avoid having actions destroyed when this routine returns # by stowing a reference in the menu itself from PyQt5.QtWidgets import QAction filter_action = QAction("Typed Commands Only", menu) filter_action.setCheckable(True) filter_action.setChecked(self.settings.typed_only) filter_action.toggled.connect( lambda arg, f=self._set_typed_only: f(arg)) menu.addAction(filter_action) select_action = QAction("Leave Failed Command Highlighted", menu) select_action.setCheckable(True) select_action.setChecked(self.settings.select_failed) select_action.toggled.connect( lambda arg, f=self._set_select_failed: f(arg)) menu.addAction(select_action) def on_combobox(self, event): val = self.text.GetValue() if val == self.show_history_label: self.cmd_clear() self.history_dialog.window.shown = True elif val == self.compact_label: self.cmd_clear() prev_cmd = None unique_cmds = [] for cmd in self.history_dialog._history: if cmd != prev_cmd: unique_cmds.append(cmd) prev_cmd = cmd self.history_dialog._history.replace(unique_cmds) self.history_dialog.populate() else: event.Skip() def text_changed(self, text): if text == self.show_history_label: self.cmd_clear() if not self._in_init: self.history_dialog.window.shown = True elif text == self.compact_label: self.cmd_clear() prev_cmd = None unique_cmds = [] for cmd in self.history_dialog._history: if cmd != prev_cmd: unique_cmds.append(cmd) prev_cmd = cmd self.history_dialog._history.replace(unique_cmds) self.history_dialog.populate() def execute(self): from contextlib import contextmanager @contextmanager def processing_command(line_edit, cmd_text, command_worked, select_failed): line_edit.blockSignals(True) self._processing_command = True # as per the docs for contextmanager, the yield needs # to be in a try/except if the exit code is to execute # after errors try: yield finally: line_edit.blockSignals(False) line_edit.setText(cmd_text) if command_worked[0] or select_failed: line_edit.selectAll() self._processing_command = False session = self.session logger = session.logger text = self.text.lineEdit().text() logger.status("") from chimerax.core import errors from chimerax.core.commands import Command from html import escape for cmd_text in text.split("\n"): if not cmd_text: continue # don't select the text if the command failed, so that # an accidental keypress won't erase the command, which # probably needs to be edited to work command_worked = [False] with processing_command(self.text.lineEdit(), cmd_text, command_worked, self.settings.select_failed): try: self._just_typed_command = cmd_text cmd = Command(session) cmd.run(cmd_text) command_worked[0] = True except SystemExit: # TODO: somehow quit application raise except errors.UserError as err: logger.status(str(err), color="crimson") from chimerax.core.logger import error_text_format logger.info(error_text_format % escape(str(err)), is_html=True) except BaseException: raise self.set_focus() def set_focus(self): from PyQt5.QtCore import Qt self.text.lineEdit().setFocus(Qt.OtherFocusReason) @classmethod def get_singleton(cls, session, **kw): from chimerax.core import tools return tools.get_singleton(session, CommandLine, 'Command Line Interface', **kw) def _command_started_cb(self, trig_name, cmd_text): # the self._processing_command test is necessary when multiple commands # separated by semicolons are typed in order to prevent putting the # second and later commands into the command history, since we will get # triggers for each command in the line if self._just_typed_command or not self._processing_command: self.history_dialog.add(self._just_typed_command or cmd_text, typed=self._just_typed_command is not None) self.text.lineEdit().selectAll() self._just_typed_command = None def _run_startup_commands(self, *args): # log the commands; but prevent them from going into command history... self._processing_command = True from chimerax.core.commands import run from chimerax.core.errors import UserError try: for cmd_text in self.settings.startup_commands: run(self.session, cmd_text) except UserError as err: self.session.logger.status( "Error running startup command '%s': %s" % (cmd_text, str(err)), color="crimson", log=True) except Exception: self._processing_command = False raise self._processing_command = False def _set_select_failed(self, select_failed): self.settings.select_failed = select_failed def _set_typed_only(self, typed_only): self.settings.typed_only = typed_only self.history_dialog.set_typed_only(typed_only)
def __init__(self, session, tool_name, value_name, title, value_range=(1, 10), loop=True, pause_frames=50, pause_when_recording=True, movie_filename='movie.mp4', movie_framerate=25, placement='side'): ToolInstance.__init__(self, session, tool_name) self.value_range = value_range self.loop = loop self.pause_frames = pause_frames self.pause_when_recording = pause_when_recording self._pause_count = 0 self.movie_filename = movie_filename self.movie_framerate = movie_framerate self._last_shown_value = None self._play_handler = None self.recording = False self._block_update = False self.display_name = title # Text shown on panel title-bar from chimerax.ui import MainToolWindow tw = MainToolWindow(self) self.tool_window = tw parent = tw.ui_area from PyQt5.QtWidgets import QHBoxLayout, QLabel, QSpinBox, QSlider, QPushButton from PyQt5.QtGui import QPixmap, QIcon from PyQt5.QtCore import Qt layout = QHBoxLayout() layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(4) vl = QLabel(value_name) layout.addWidget(vl) self.value_box = vb = QSpinBox() vb.setRange(value_range[0], value_range[1]) vb.valueChanged.connect(self.value_changed_cb) layout.addWidget(vb) self.slider = sl = QSlider(Qt.Horizontal) sl.setRange(value_range[0], value_range[1]) sl.valueChanged.connect(self.slider_moved_cb) layout.addWidget(sl) self.play_button = pb = QPushButton() pb.setCheckable(True) pb.pressed.connect(self.play_cb) layout.addWidget(pb) self.record_button = rb = QPushButton() rb.setCheckable(True) rb.clicked.connect(self.record_cb) layout.addWidget(rb) parent.setLayout(layout) self.set_button_icon(play=True, record=True) tw.manage(placement=placement)
class ModellerResultsViewer(ToolInstance): """ Viewer displays the models/results generated by Modeller""" help = "help:user/tools/modeller.html#output" def __init__(self, session, tool_name, models=None, attr_names=None): """ if 'models' is None, then we are being restored from a session and set_state_from_snapshot will be called later. """ ToolInstance.__init__(self, session, tool_name) if models is None: return self._finalize_init(session, models, attr_names) def _finalize_init(self, session, models, attr_names, scores_fetched=False): self.models = models self.attr_names = attr_names from chimerax.core.models import REMOVE_MODELS self.handlers = [ session.triggers.add_handler(REMOVE_MODELS, self._models_removed_cb) ] from chimerax.ui import MainToolWindow self.tool_window = MainToolWindow(self, close_destroys=False, statusbar=False) self.tool_window.fill_context_menu = self.fill_context_menu parent = self.tool_window.ui_area from PyQt5.QtWidgets import QTableWidget, QVBoxLayout, QAbstractItemView, QWidget, QPushButton layout = QVBoxLayout() layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) parent.setLayout(layout) self.table = QTableWidget() self.table.setSortingEnabled(True) self.table.keyPressEvent = session.ui.forward_keystroke self.table.setSelectionBehavior(QAbstractItemView.SelectRows) self.table.setSelectionMode(QAbstractItemView.ExtendedSelection) self.table.itemSelectionChanged.connect(self._table_selection_cb) self.table.setEditTriggers(QAbstractItemView.NoEditTriggers) layout.addWidget(self.table) layout.setStretchFactor(self.table, 1) self._fill_table() self.tool_window.manage('side') self.scores_fetched = scores_fetched for m in models: self.handlers.append( m.triggers.add_handler("changes", self._changes_cb)) def delete(self): for handler in self.handlers: handler.remove() self.row_item_lookup = {} ToolInstance.delete(self) def fetch_additional_scores(self, refresh=False): self.scores_fetched = True from chimerax.core.commands import run, concise_model_spec run( self.session, "modeller scores %s refresh %s" % (concise_model_spec( self.session, self.models), str(refresh).lower())) def fill_context_menu(self, menu, x, y): from PyQt5.QtWidgets import QAction if self.scores_fetched: refresh_action = QAction("Refresh Scores", menu) refresh_action.triggered.connect( lambda arg: self.fetch_additional_scores(refresh=True)) menu.addAction(refresh_action) else: fetch_action = QAction("Fetch Additional Scores", menu) fetch_action.triggered.connect( lambda arg: self.fetch_additional_scores()) menu.addAction(fetch_action) @classmethod def restore_snapshot(cls, session, data): inst = super().restore_snapshot(session, data['ToolInstance']) inst._finalize_init(session, data['models'], data['attr_names'], data['scores_fetched']) return inst SESSION_SAVE = True def take_snapshot(self, session, flags): data = { 'ToolInstance': ToolInstance.take_snapshot(self, session, flags), 'models': self.models, 'attr_names': self.attr_names, 'scores_fetched': self.scores_fetched } return data def _changes_cb(self, trig_name, trig_data): structure, changes_obj = trig_data need_update = False if changes_obj.modified_structures(): for reason in changes_obj.structure_reasons(): if reason.startswith("modeller_") and reason.endswith( " changed"): need_update = True attr_name = reason[:-8] if attr_name not in self.attr_names: self.attr_names.append(attr_name) if need_update: # atomic changes are already collated, so don't need to delay table update self._fill_table() def _fill_table(self, *args): self.table.clear() self.table.setColumnCount(len(self.attr_names) + 1) self.table.setHorizontalHeaderLabels( ["Model"] + [attr_name[9:].replace('_', ' ') for attr_name in self.attr_names]) self.table.setRowCount(len(self.models)) from PyQt5.QtWidgets import QTableWidgetItem for row, m in enumerate(self.models): item = QTableWidgetItem('#' + m.id_string) self.table.setItem(row, 0, item) for c, attr_name in enumerate(self.attr_names): self.table.setItem( row, c + 1, QTableWidgetItem("%g" % getattr(m, attr_name) if hasattr( m, attr_name) else "")) for i in range(self.table.columnCount()): self.table.resizeColumnToContents(i) def _models_removed_cb(self, *args): remaining = [m for m in self.models if m.id is not None] if remaining == self.models: return self.models = remaining if not self.models: self.delete() else: self._fill_table() def _table_selection_cb(self): rows = set([index.row() for index in self.table.selectedIndexes()]) sel_ids = set([self.table.item(r, 0).text() for r in rows]) for m in self.models: m.display = ('#' + m.id_string) in sel_ids or not sel_ids
class ConeAngle(ToolInstance): help = "https://github.com/QChASM/SEQCROW/wiki/Cone-Angle-Tool" def __init__(self, session, name): super().__init__(session, name) from chimerax.ui import MainToolWindow self.tool_window = MainToolWindow(self) self.settings = _ConeAngleSettings(self.session, name) self.ligands = dict() self._build_ui() def _build_ui(self): layout = QFormLayout() self.cone_option = QComboBox() self.cone_option.addItems(["Tolman (Unsymmetrical)", "Exact"]) ndx = self.cone_option.findText(self.settings.cone_option, Qt.MatchExactly) self.cone_option.setCurrentIndex(ndx) layout.addRow("method:", self.cone_option) self.radii_option = QComboBox() self.radii_option.addItems(["Bondi", "UMN"]) ndx = self.radii_option.findText(self.settings.radii, Qt.MatchExactly) self.radii_option.setCurrentIndex(ndx) layout.addRow("radii:", self.radii_option) self.display_cone = QCheckBox() self.display_cone.setChecked(self.settings.display_cone) layout.addRow("show cone:", self.display_cone) self.display_radii = QCheckBox() self.display_radii.setChecked(self.settings.display_radii) layout.addRow("show radii:", self.display_radii) set_ligand_button = QPushButton("set ligand to current selection") set_ligand_button.clicked.connect(self.set_ligand) layout.addRow(set_ligand_button) self.set_ligand_button = set_ligand_button calc_cone_button = QPushButton( "calculate cone angle for ligand on selected center") calc_cone_button.clicked.connect(self.calc_cone) layout.addRow(calc_cone_button) self.calc_cone_button = calc_cone_button remove_cone_button = QPushButton("remove cone visualizations") remove_cone_button.clicked.connect(self.del_cone) layout.addRow(remove_cone_button) self.remove_cone_button = remove_cone_button self.table = QTableWidget() self.table.setColumnCount(3) self.table.setHorizontalHeaderLabels([ 'model', 'center', 'cone angle (°)', ]) self.table.setSelectionBehavior(QTableWidget.SelectRows) self.table.setEditTriggers(QTableWidget.NoEditTriggers) self.table.resizeColumnToContents(0) self.table.resizeColumnToContents(1) self.table.resizeColumnToContents(2) self.table.horizontalHeader().setSectionResizeMode( 2, QHeaderView.Stretch) layout.addRow(self.table) menu = QMenuBar() export = menu.addMenu("&Export") clear = QAction("Clear data table", self.tool_window.ui_area) clear.triggered.connect(self.clear_table) export.addAction(clear) copy = QAction("&Copy CSV to clipboard", self.tool_window.ui_area) copy.triggered.connect(self.copy_csv) shortcut = QKeySequence(Qt.CTRL + Qt.Key_C) copy.setShortcut(shortcut) export.addAction(copy) save = QAction("&Save CSV...", self.tool_window.ui_area) save.triggered.connect(self.save_csv) #this shortcut interferes with main window's save shortcut #I've tried different shortcut contexts to no avail #thanks Qt... #shortcut = QKeySequence(Qt.CTRL + Qt.Key_S) #save.setShortcut(shortcut) #save.setShortcutContext(Qt.WidgetShortcut) export.addAction(save) delimiter = export.addMenu("Delimiter") comma = QAction("comma", self.tool_window.ui_area, checkable=True) comma.setChecked(self.settings.delimiter == "comma") comma.triggered.connect(lambda *args, delim="comma": self.settings. __setattr__("delimiter", delim)) delimiter.addAction(comma) tab = QAction("tab", self.tool_window.ui_area, checkable=True) tab.setChecked(self.settings.delimiter == "tab") tab.triggered.connect(lambda *args, delim="tab": self.settings. __setattr__("delimiter", delim)) delimiter.addAction(tab) space = QAction("space", self.tool_window.ui_area, checkable=True) space.setChecked(self.settings.delimiter == "space") space.triggered.connect(lambda *args, delim="space": self.settings. __setattr__("delimiter", delim)) delimiter.addAction(space) semicolon = QAction("semicolon", self.tool_window.ui_area, checkable=True) semicolon.setChecked(self.settings.delimiter == "semicolon") semicolon.triggered.connect(lambda *args, delim="semicolon": self. settings.__setattr__("delimiter", delim)) delimiter.addAction(semicolon) add_header = QAction("&Include CSV header", self.tool_window.ui_area, checkable=True) add_header.setChecked(self.settings.include_header) add_header.triggered.connect(self.header_check) export.addAction(add_header) comma.triggered.connect( lambda *args, action=tab: action.setChecked(False)) comma.triggered.connect( lambda *args, action=space: action.setChecked(False)) comma.triggered.connect( lambda *args, action=semicolon: action.setChecked(False)) tab.triggered.connect( lambda *args, action=comma: action.setChecked(False)) tab.triggered.connect( lambda *args, action=space: action.setChecked(False)) tab.triggered.connect( lambda *args, action=semicolon: action.setChecked(False)) space.triggered.connect( lambda *args, action=comma: action.setChecked(False)) space.triggered.connect( lambda *args, action=tab: action.setChecked(False)) space.triggered.connect( lambda *args, action=semicolon: action.setChecked(False)) semicolon.triggered.connect( lambda *args, action=comma: action.setChecked(False)) semicolon.triggered.connect( lambda *args, action=tab: action.setChecked(False)) semicolon.triggered.connect( lambda *args, action=space: action.setChecked(False)) menu.setNativeMenuBar(False) self._menu = menu layout.setMenuBar(menu) menu.setVisible(True) self.tool_window.ui_area.setLayout(layout) self.tool_window.manage(None) def clear_table(self): are_you_sure = QMessageBox.question( None, "Clear table?", "Are you sure you want to clear the data table?", ) if are_you_sure != QMessageBox.Yes: return self.table.setRowCount(0) def header_check(self, state): """user has [un]checked the 'include header' option on the menu""" if state: self.settings.include_header = True else: self.settings.include_header = False def get_csv(self): if self.settings.delimiter == "comma": delim = "," elif self.settings.delimiter == "space": delim = " " elif self.settings.delimiter == "tab": delim = "\t" elif self.settings.delimiter == "semicolon": delim = ";" if self.settings.include_header: s = delim.join(["model", "center_atom", "cone_angle"]) s += "\n" else: s = "" for i in range(0, self.table.rowCount()): s += delim.join([ item.data(Qt.DisplayRole) for item in [self.table.item(i, j) for j in range(0, 3)] ]) s += "\n" return s def copy_csv(self): app = QApplication.instance() clipboard = app.clipboard() csv = self.get_csv() clipboard.setText(csv) self.session.logger.status("copied to clipboard") def save_csv(self): """save data on current tab to CSV file""" filename, _ = QFileDialog.getSaveFileName(filter="CSV Files (*.csv)") if filename: s = self.get_csv() with open(filename, 'w') as f: f.write(s.strip()) self.session.logger.status("saved to %s" % filename) def del_cone(self): for model in self.session.models.list(type=Generic3DModel): if model.name.startswith("Cone angle"): model.delete() def set_ligand(self, *args): self.ligands = {} for atom in selected_atoms(self.session): if atom.structure not in self.ligands: self.ligands[atom.structure] = [] self.ligands[atom.structure].append(atom) self.session.logger.status("set ligand to current selection") def calc_cone(self, *args): self.settings.cone_option = self.cone_option.currentText() self.settings.radii = self.radii_option.currentText() self.settings.display_radii = self.display_radii.checkState( ) == Qt.Checked self.settings.display_cone = self.display_cone.checkState( ) == Qt.Checked if self.cone_option.currentText() == "Tolman (Unsymmetrical)": method = "tolman" else: method = self.cone_option.currentText() radii = self.radii_option.currentText() return_cones = self.display_cone.checkState() == Qt.Checked display_radii = self.display_radii.checkState() == Qt.Checked # self.table.setRowCount(0) for center_atom in selected_atoms(self.session): rescol = ResidueCollection(center_atom.structure) at_center = rescol.find_exact(AtomSpec(center_atom.atomspec))[0] if center_atom.structure in self.ligands: comp = Component( rescol.find([ AtomSpec(atom.atomspec) for atom in self.ligands[center_atom.structure] ]), to_center=rescol.find_exact(AtomSpec( center_atom.atomspec)), key_atoms=rescol.find(BondedTo(at_center)), ) else: comp = Component( rescol.find(NotAny(at_center)), to_center=rescol.find_exact(AtomSpec( center_atom.atomspec)), key_atoms=rescol.find(BondedTo(at_center)), ) cone_angle = comp.cone_angle( center=rescol.find(AtomSpec(center_atom.atomspec)), method=method, radii=radii, return_cones=return_cones, ) if return_cones: cone_angle, cones = cone_angle s = ".transparency 0.5\n" for cone in cones: apex, base, radius = cone s += ".cone %6.3f %6.3f %6.3f %6.3f %6.3f %6.3f %.3f open\n" % ( *apex, *base, radius) stream = BytesIO(bytes(s, "utf-8")) bild_obj, status = read_bild(self.session, stream, "Cone angle %s" % center_atom) self.session.models.add(bild_obj, parent=center_atom.structure) if display_radii: s = ".note radii\n" s += ".transparency 75\n" color = None for atom in comp.atoms: chix_atom = atom.chix_atom if radii.lower() == "umn": r = VDW_RADII[chix_atom.element.name] elif radii.lower() == "bondi": r = BONDI_RADII[chix_atom.element.name] if color is None or chix_atom.color != color: color = chix_atom.color rgb = [x / 255. for x in chix_atom.color] rgb.pop(-1) s += ".color %f %f %f\n" % tuple(rgb) s += ".sphere %f %f %f %f\n" % (*chix_atom.coord, r) stream = BytesIO(bytes(s, "utf-8")) bild_obj, status = read_bild(self.session, stream, "Cone angle radii") self.session.models.add(bild_obj, parent=center_atom.structure) row = self.table.rowCount() self.table.insertRow(row) name = QTableWidgetItem() name.setData(Qt.DisplayRole, center_atom.structure.name) self.table.setItem(row, 0, name) center = QTableWidgetItem() center.setData(Qt.DisplayRole, center_atom.atomspec) self.table.setItem(row, 1, center) ca = QTableWidgetItem() ca.setData(Qt.DisplayRole, "%.2f" % cone_angle) self.table.setItem(row, 2, ca) self.table.resizeColumnToContents(0) self.table.resizeColumnToContents(1) self.table.resizeColumnToContents(2)
class ModellerLauncher(ToolInstance): """Generate the inputs needed by Modeller for comparative modeling""" help = "help:user/tools/modeller.html" SESSION_SAVE = False def __init__(self, session, tool_name): ToolInstance.__init__(self, session, tool_name) from chimerax.ui import MainToolWindow self.tool_window = MainToolWindow(self, close_destroys=False, statusbar=False) parent = self.tool_window.ui_area from PyQt5.QtWidgets import QListWidget, QFormLayout, QAbstractItemView, QGroupBox, QVBoxLayout from PyQt5.QtWidgets import QDialogButtonBox as qbbox interface_layout = QVBoxLayout() interface_layout.setContentsMargins(0, 0, 0, 0) interface_layout.setSpacing(0) parent.setLayout(interface_layout) alignments_area = QGroupBox("Sequence alignments") interface_layout.addWidget(alignments_area) interface_layout.setStretchFactor(alignments_area, 1) alignments_layout = QVBoxLayout() alignments_layout.setContentsMargins(0, 0, 0, 0) alignments_area.setLayout(alignments_layout) self.alignment_list = AlignmentListWidget(session) self.alignment_list.setSelectionMode( QAbstractItemView.ExtendedSelection) self.alignment_list.keyPressEvent = session.ui.forward_keystroke self.alignment_list.value_changed.connect(self._list_selection_cb) self.alignment_list.alignments_changed.connect( self._update_sequence_menus) alignments_layout.addWidget(self.alignment_list) alignments_layout.setStretchFactor(self.alignment_list, 1) targets_area = QGroupBox("Target sequences") self.targets_layout = QFormLayout() targets_area.setLayout(self.targets_layout) interface_layout.addWidget(targets_area) self.seq_menu = {} self._update_sequence_menus(session.alignments.alignments) options_area = QGroupBox("Options") options_layout = QVBoxLayout() options_layout.setContentsMargins(0, 0, 0, 0) options_area.setLayout(options_layout) interface_layout.addWidget(options_area) interface_layout.setStretchFactor(options_area, 2) from chimerax.ui.options import CategorizedSettingsPanel, BooleanOption, IntOption, PasswordOption, \ OutputFolderOption panel = CategorizedSettingsPanel(category_sorting=False, buttons=False) options_layout.addWidget(panel) from .settings import get_settings settings = get_settings(session) panel.add_option( "Basic", BooleanOption( "Make multichain model from multichain template", settings.multichain, None, balloon= "If false, all chains (templates) associated with an alignment will be used in combination\n" "to model the target sequence of that alignment, i.e. a monomer will be generated from the\n" "alignment. If true, the target sequence will be modeled from each template, i.e. a multimer\n" "will be generated from the alignment (assuming multiple chains are associated).", attr_name="multichain", settings=settings)) max_models = 1000 panel.add_option( "Basic", IntOption( "Number of models", settings.num_models, None, attr_name="num_models", settings=settings, min=1, max=max_models, balloon= "Number of model structures to generate. Must be no more than %d.\n" "Warning: please consider the calculation time" % max_models)) key = "" if settings.license_key is None else settings.license_key panel.add_option( "Basic", PasswordOption( "Modeller license key", key, None, attr_name="license_key", settings=settings, balloon= "Your Modeller license key. You can obtain a license key by registering at the Modeller web site" )) panel.add_option( "Advanced", BooleanOption( "Use fast/approximate mode (produces only one model)", settings.fast, None, attr_name="fast", settings=settings, balloon= "If enabled, use a fast approximate method to generate a single model.\n" "Typically use to get a rough idea what the model will look like or\n" "to check that the alignment is reasonable.")) panel.add_option( "Advanced", BooleanOption( "Include non-water HETATM residues from template", settings.het_preserve, None, attr_name="het_preserve", settings=settings, balloon= "If enabled, all non-water HETATM residues in the template\n" "structure(s) will be transferred into the generated models.")) panel.add_option( "Advanced", BooleanOption( "Build models with hydrogens", settings.hydrogens, None, attr_name="hydrogens", settings=settings, balloon= "If enabled, the generated models will include hydrogen atoms.\n" "Otherwise, only heavy atom coordinates will be built.\n" "Increases computation time by approximately a factor of 4.")) panel.add_option( "Advanced", OutputFolderOption( "Temporary folder location (optional)", settings.temp_path, None, attr_name="temp_path", settings=settings, balloon= "Specify a folder for temporary files. If not specified,\n" "a location will be generated automatically.")) panel.add_option( "Advanced", BooleanOption( "Include water molecules from template", settings.water_preserve, None, attr_name="water_preserve", settings=settings, balloon="If enabled, all water molecules in the template\n" "structure(s) will be included in the generated models.")) from PyQt5.QtCore import Qt from chimerax.ui.widgets import Citation interface_layout.addWidget(Citation( session, "A. Sali and T.L. Blundell.\n" "Comparative protein modelling by satisfaction of spatial restraints.\n" "J. Mol. Biol. 234, 779-815, 1993.", prefix="Publications using Modeller results should cite:", pubmed_id=18428767), alignment=Qt.AlignCenter) bbox = qbbox(qbbox.Ok | qbbox.Cancel | qbbox.Help) bbox.accepted.connect(self.launch_modeller) bbox.rejected.connect(self.delete) from chimerax.core.commands import run bbox.helpRequested.connect( lambda run=run, ses=session: run(ses, "help " + self.help)) interface_layout.addWidget(bbox) self.tool_window.manage(None) def delete(self): ToolInstance.delete(self) def launch_modeller(self): from chimerax.core.commands import run, FileNameArg, StringArg from chimerax.core.errors import UserError alignments = self.alignment_list.value if not alignments: raise UserError("No alignments chosen for modeling") aln_seq_args = [] for aln in alignments: seq_menu = self.seq_menu[aln] seq = seq_menu.value if not seq: raise UserError("No target sequence chosen for alignment %s" % aln.ident) aln_seq_args.append( StringArg.unparse("%s:%d" % (aln.ident, aln.seqs.index(seq) + 1))) from .settings import get_settings settings = get_settings(self.session) run( self.session, "modeller comparative %s multichain %s numModels %d fast %s hetPreserve %s" " hydrogens %s%s waterPreserve %s" % (" ".join(aln_seq_args), repr( settings.multichain).lower(), settings.num_models, repr(settings.fast).lower(), repr(settings.het_preserve).lower(), repr(settings.hydrogens).lower(), " tempPath %s" % FileNameArg.unparse(settings.temp_path) if settings.temp_path else "", repr(settings.water_preserve).lower())) self.delete() def _list_selection_cb(self): layout = self.targets_layout while layout.count() > 0: item = layout.takeAt(0) if not item: break widget = item.widget() if isinstance(widget, AlignSeqMenuButton): widget.setHidden(True) else: widget.deleteLater() for sel_aln in self.alignment_list.value: mb = self.seq_menu[sel_aln] mb.setHidden(False) layout.addRow(sel_aln.ident, mb) def _update_sequence_menus(self, alignments): alignment_set = set(alignments) for aln, mb in list(self.seq_menu.items()): if aln not in alignment_set: row, role = self.targets_layout.getWidgetPosition(mb) if row >= 0: self.targets_layout.removeRow(row) del self.seq_menu[aln] for aln in alignments: if aln not in self.seq_menu: self.seq_menu[aln] = AlignSeqMenuButton( aln, no_value_button_text="No target chosen")
def __init__(self, session, tool_name): ToolInstance.__init__(self, session, tool_name) self.display_name = 'Marker Placement' from chimerax.ui import MainToolWindow tw = MainToolWindow(self, close_destroys=False) self.tool_window = tw parent = tw.ui_area from PyQt5.QtWidgets import QFrame, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QLineEdit, QSizePolicy, QCheckBox playout = QVBoxLayout(parent) playout.setContentsMargins(0,0,0,0) playout.setSpacing(0) parent.setLayout(playout) f = QFrame(parent) f.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum) playout.addWidget(f) layout = QVBoxLayout(f) layout.setContentsMargins(0,0,0,0) layout.setSpacing(0) f.setLayout(layout) # Marker and link color and radius mf = QFrame(f) mf.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum) layout.addWidget(mf) mm_layout = QHBoxLayout(mf) mm_layout.setContentsMargins(0,0,0,0) mm_layout.setSpacing(5) mf.setLayout(mm_layout) ml = QLabel('Marker color', mf) mm_layout.addWidget(ml) from chimerax.ui.widgets import ColorButton self._marker_color = mc = ColorButton(mf, max_size = (16,16)) mc.color_changed.connect(self._marker_color_changed) mm_layout.addWidget(mc) rl = QLabel(' radius', mf) mm_layout.addWidget(rl) self._marker_radius = mr = QLineEdit('', mf) mr.setMaximumWidth(40) mr.returnPressed.connect(self._marker_radius_changed) mm_layout.addWidget(mr) mm_layout.addSpacing(20) ml = QLabel('Link color', mf) mm_layout.addWidget(ml) from chimerax.ui.widgets import ColorButton self._link_color = lc = ColorButton(mf, max_size = (16,16)) lc.color_changed.connect(self._link_color_changed) mm_layout.addWidget(lc) rl = QLabel(' radius', mf) mm_layout.addWidget(rl) self._link_radius = lr = QLineEdit('', mf) lr.setMaximumWidth(40) lr.returnPressed.connect(self._link_radius_changed) mm_layout.addWidget(lr) mm_layout.addStretch(1) # Extra space at end # Link consecutive markers checkbutton self.link_new_button = lm = QCheckBox('Link new marker to selected marker', f) lm.stateChanged.connect(self.link_new_cb) layout.addWidget(lm) layout.addSpacing(5) hl = QLabel('Place markers using mouse modes in the Markers toolbar') layout.addWidget(hl) self.update_settings() tw.manage(placement="side")
class EnergyPlot(ToolInstance): SESSION_ENDURING = False SESSION_SAVE = False def __init__(self, session, model, filereader): super().__init__(session, model.name) from chimerax.ui import MainToolWindow self.tool_window = MainToolWindow(self) self.structure = model self.filereader = filereader self.display_name = "Thing per iteration for %s" % model.name self._last_mouse_xy = None self._dragged = False self._min_drag = 10 # pixels self._drag_mode = None self._build_ui() self.press = None self.drag_prev = None self.dragging = False self._model_closed = self.session.triggers.add_handler( REMOVE_MODELS, self.check_closed_models) def _build_ui(self): layout = QGridLayout() self.figure = Figure(figsize=(2, 2)) self.canvas = Canvas(self.figure) ax = self.figure.add_axes((0.15, 0.20, 0.80, 0.70)) fr = self.filereader if fr.all_geom is None: self.opened = False return data = [] for step in fr.all_geom: info = [ item for item in step if isinstance(item, dict) and "energy" in item ] if len(info) < 1: #we will be unable to load an enegy plot because some structure does not have an associated energy self.opened = False self.session.logger.error( "not enough iterations to plot - %i found" % len(info)) return else: info = info[0] data.append(info["energy"]) self.ys = data se = np.ptp(data) self.nrg_plot = ax.plot(self.structure.coordset_ids, data, marker='o', c='gray', markersize=3) self.nrg_plot = self.nrg_plot[0] ax.set_xlabel('iteration') ax.set_ylabel(r'energy ($E_h$)') ax.set_ylim(bottom=(min(data) - se / 10), top=(max(data) + se / 10)) minlocs = [ self.structure.coordset_ids[i] for i in range(0, self.structure.num_coordsets) if data[i] == min(data) ] mins = [min(data) for m in minlocs] maxlocs = [ self.structure.coordset_ids[i] for i in range(0, self.structure.num_coordsets) if data[i] == max(data) ] maxs = [max(data) for m in minlocs] ax.plot(minlocs, mins, marker='*', c='blue', markersize=5) ax.plot(maxlocs, maxs, marker='*', c='red', markersize=5) ax.ticklabel_format(axis='y', style='sci', scilimits=(0, 0), useOffset=True) ax.ticklabel_format(axis='x', style='plain', useOffset=False) self.canvas.mpl_connect('button_release_event', self.unclick) self.canvas.mpl_connect('button_press_event', self.onclick) self.canvas.mpl_connect('motion_notify_event', self.drag) self.canvas.mpl_connect('scroll_event', self.zoom) self.annotation = ax.annotate("", xy=(0, 0), xytext=(0, 10), textcoords="offset points", fontfamily='Arial') self.annotation.set_visible(False) ax.autoscale() self.canvas.draw() self.canvas.setMinimumWidth(400) self.canvas.setMinimumHeight(200) layout.addWidget(self.canvas) toolbar_widget = QWidget() toolbar = NavigationToolbar(self.canvas, toolbar_widget) toolbar.setMaximumHeight(32) self.toolbar = toolbar layout.addWidget(toolbar) #menu bar for saving stuff menu = QMenuBar() file = menu.addMenu("&Export") file.addAction("&Save CSV...") file.triggered.connect(self.save) menu.setNativeMenuBar(False) self._menu = menu layout.setMenuBar(menu) self.tool_window.ui_area.setLayout(layout) self.tool_window.manage(None) self.opened = True def save(self): filename, _ = QFileDialog.getSaveFileName(filter="CSV Files (*.csv)") if filename: s = "iteration,energy\n" for i, nrg in enumerate(self.ys): s += "%i,%f\n" % (self.structure.coordset_ids[i], nrg) with open(filename, 'w') as f: f.write(s.strip()) print("saved to %s" % filename) def zoom(self, event): if event.xdata is None: return a = self.figure.gca() x0, x1 = a.get_xlim() x_range = x1 - x0 xdiff = -0.05 * event.step * x_range xshift = 0.2 * (event.xdata - (x0 + x1) / 2) nx0 = x0 - xdiff + xshift nx1 = x1 + xdiff + xshift y0, y1 = a.get_ylim() y_range = y1 - y0 ydiff = -0.05 * event.step * y_range yshift = 0.2 * (event.ydata - (y0 + y1) / 2) ny0 = y0 - ydiff + yshift ny1 = y1 + ydiff + yshift a.set_xlim(nx0, nx1) a.set_ylim(ny0, ny1) self.canvas.draw() def move(self, dx, dy): win = self.tool_window.ui_area w, h = win.width(), win.height() if w == 0 or h == 0: return a = self.figure.gca() x0, x1 = a.get_xlim() xs = dx / w * (x1 - x0) nx0, nx1 = x0 - xs, x1 - xs y0, y1 = a.get_ylim() ys = dy / h * (y1 - y0) ny0, ny1 = y0 - ys, y1 - ys a.set_xlim(nx0, nx1) a.set_ylim(ny0, ny1) self.canvas.draw() def onclick(self, event): if self.toolbar.mode != "": return self.press = event.x, event.y, event.xdata, event.ydata def unclick(self, event): if self.toolbar.mode != "": return modifiers = keyboardModifiers() #matplotlib's mouse event never sets the 'key' attribute for me #That's fine. #I'll just get my key presses from pyqt5. if modifiers != Qt.NoModifier and event.button == 1: a = self.figure.gca() a.autoscale() self.canvas.draw() elif not self.dragging and event.button == 1 and event.key is None: self.change_coordset(event) self.press = None self.drag_prev = None self.dragging = False def change_coordset(self, event): if event.xdata is not None: x = int(round(event.xdata)) if x > self.structure.num_coordsets: self.structure.active_coordset_id = self.structure.num_coordsets elif x < 1: self.structure.active_coordset_id = 1 else: self.structure.active_coordset_id = x def update_label(self, ndx): self.annotation.xy = (self.structure.coordset_ids[ndx], self.ys[ndx]) if self.ys[ndx] == max(self.ys): self.annotation.set_text("%s (maxima)" % repr(self.ys[ndx])) elif self.ys[ndx] == min(self.ys): self.annotation.set_text("%s (minima)" % repr(self.ys[ndx])) else: self.annotation.set_text(repr(self.ys[ndx])) def drag(self, event): modifiers = keyboardModifiers() if self.toolbar.mode != "": return #matplotlib's mouse event never sets the 'key' attribute for me #That's fine. #I'll just get my key presses from pyqt5. elif modifiers != Qt.NoModifier: return elif event.button == 1: return self.change_coordset(event) elif self.press is None: vis = self.annotation.get_visible() on_item, ndx = self.nrg_plot.contains(event) if on_item: self.update_label(ndx['ind'][0]) self.annotation.set_visible(True) self.canvas.draw() else: if vis: self.annotation.set_visible(False) self.canvas.draw() return elif event.button != 2: return x, y, xpress, ypress = self.press dx = event.x - x dy = event.y - y drag_dist = np.linalg.norm([dx, dy]) if self.dragging or drag_dist >= self._min_drag or event.button == 2: self.dragging = True if self.drag_prev is not None: x, y, xpress, ypress = self.drag_prev dx = event.x - x dy = event.y - y self.drag_prev = event.x, event.y, event.xdata, event.ydata self.move(dx, dy) def display_help(self): """Show the help for this tool in the help viewer.""" from chimerax.core.commands import run run(self.session, 'open %s' % self.help if self.help is not None else "") def check_closed_models(self, name, models): if self.structure in models: self.delete() def delete(self): self.session.triggers.remove_handler(self._model_closed) super().delete() def close(self): self.session.triggers.remove_handler(self._model_closed) super().close()
class bogusUI(ToolInstance): SIZE = (500, 25) _PageTemplate = """<html> <head> <title>Select Models</title> <script> function action(button) { window.location.href = "bogus:_action:" + button; } </script> <style> .refresh { color: blue; font-size: 80%; font-family: monospace; } </style> </head> <body> <h2>Select Models <a href="bogus:_refresh" class="refresh">refresh</a></h2> MODEL_SELECTION <p> ACTION_BUTTONS </body> </html>""" def __init__(self, session): ToolInstance.__init__(self, session, "Bogus Toolshed-demo Tool") self.display_name = "Open Models" from chimerax.ui import MainToolWindow self.tool_window = MainToolWindow(self, size=self.SIZE) parent = self.tool_window.ui_area # UI content code from wx import html2 import wx self.webview = html2.WebView.New(parent, wx.ID_ANY, size=self.SIZE) self.webview.Bind(html2.EVT_WEBVIEW_NAVIGATING, self._on_navigating, id=self.webview.GetId()) sizer = wx.BoxSizer(wx.VERTICAL) sizer.Add(self.webview, 1, wx.EXPAND) parent.SetSizerAndFit(sizer) self.tool_window.manage(placement="side") # Add triggers for model addition/removal from chimerax.core.models import ADD_MODELS, REMOVE_MODELS self._handlers = [ session.triggers.add_handler(ADD_MODELS, self._make_page), session.triggers.add_handler(REMOVE_MODELS, self._make_page) ] self._make_page() def _make_page(self, *args): models = self.session.models from io import StringIO page = self._PageTemplate # Construct model selector s = StringIO() print("<select multiple=\"1\">", file=s) for model in models.list(): name = '.'.join([str(n) for n in model.id]) print("<option value=\"%s\">%s</option>" % (name, name), file=s) print("</select>", file=s) page = page.replace("MODEL_SELECTION", s.getvalue()) # Construct action buttons s = StringIO() for action in ["BLAST"]: print("<button type=\"button\"" "onclick=\"action('%s')\">%s</button>" % (action, action), file=s) page = page.replace("ACTION_BUTTONS", s.getvalue()) # Update display self.webview.SetPage(page, "") def _on_navigating(self, event): session = self.session # Handle event url = event.GetURL() if url.startswith("bogus:"): event.Veto() parts = url.split(':') method = getattr(self, parts[1]) args = parts[2:] method(session, *args) # # Callbacks from HTML # def _refresh(self, session): self._make_page() def _action(self, session, action): print("bogus action button clicked: %s" % action)
def __init__(self, session, tool_name): self._ses = session ToolInstance.__init__(self, session, tool_name) from .settings import BugReporterSettings self.settings = BugReporterSettings(session, 'Bug Reporter') from chimerax.ui import MainToolWindow tw = MainToolWindow(self) self.tool_window = tw parent = tw.ui_area parent.setMinimumWidth(600) from PyQt5.QtWidgets import QGridLayout, QLabel, QPushButton, QLineEdit, QTextEdit from PyQt5.QtWidgets import QWidget, QHBoxLayout, QCheckBox from PyQt5.QtCore import Qt layout = QGridLayout(parent) layout.setContentsMargins(3, 3, 3, 3) layout.setHorizontalSpacing(3) layout.setVerticalSpacing(3) parent.setLayout(layout) row = 1 intro = ''' <center><h1>Report a Bug</h1></center> <p>Thank you for using our feedback system. Feedback is greatly appreciated and plays a crucial role in the development of ChimeraX.</p> <p><b>Note</b>: We do not automatically collect any personal information or the data you were working with when the problem occurred. Providing your e-mail address is optional, but will allow us to inform you of a fix or to ask questions, if needed. Attaching data may also be helpful. However, any information or data you wish to keep confidential should be sent separately (not using this form).</p> ''' il = QLabel(intro) il.setWordWrap(True) layout.addWidget(il, row, 1, 1, 2) row += 1 cnl = QLabel('Contact Name:') cnl.setAlignment(Qt.AlignRight | Qt.AlignVCenter) layout.addWidget(cnl, row, 1) self.contact_name = cn = QLineEdit(self.settings.contact_name) layout.addWidget(cn, row, 2) row += 1 eml = QLabel('Email Address:') eml.setAlignment(Qt.AlignRight | Qt.AlignVCenter) layout.addWidget(eml, row, 1) self.email_address = em = QLineEdit(self.settings.email_address) layout.addWidget(em, row, 2) row += 1 class TextEdit(QTextEdit): def __init__(self, text, initial_line_height): self._lines = initial_line_height QTextEdit.__init__(self, text) def sizeHint(self): from PyQt5.QtCore import QSize fm = self.fontMetrics() h = self._lines * fm.lineSpacing() + fm.ascent() size = QSize(-1, h) return size def minimumSizeHint(self): from PyQt5.QtCore import QSize return QSize(1, 1) dl = QLabel('Description:') dl.setAlignment(Qt.AlignRight | Qt.AlignVCenter) layout.addWidget(dl, row, 1) self.description = d = TextEdit('', 3) d.setText( '<font color=blue>(Describe the actions that caused this problem to occur here)</font>' ) layout.addWidget(d, row, 2) row += 1 gil = QLabel('Gathered Information:') gil.setAlignment(Qt.AlignRight | Qt.AlignVCenter) layout.addWidget(gil, row, 1) self.gathered_info = gi = TextEdit('', 3) import sys info = self.opengl_info() if sys.platform == 'win32': info += _win32_info() elif sys.platform == 'linux': info += _linux_info() elif sys.platform == 'darwin': info += _darwin_info() info += _qt_info(session) info += _package_info() gi.setText(info) layout.addWidget(gi, row, 2) row += 1 fal = QLabel('File Attachment:') fal.setAlignment(Qt.AlignRight | Qt.AlignVCenter) layout.addWidget(fal, row, 1) fb = QWidget() layout.addWidget(fb, row, 2) fbl = QHBoxLayout(fb) fbl.setSpacing(3) fbl.setContentsMargins(0, 0, 0, 0) self.attachment = fa = QLineEdit('') fbl.addWidget(fa) fab = QPushButton('Browse') fab.clicked.connect(lambda e: self.file_browse()) fbl.addWidget(fab) row += 1 pl = QLabel('Platform:') pl.setAlignment(Qt.AlignRight | Qt.AlignVCenter) layout.addWidget(pl, row, 1) from platform import platform self.platform = p = QLineEdit(platform()) p.setReadOnly(True) layout.addWidget(p, row, 2) row += 1 vl = QLabel('ChimeraX Version:') vl.setAlignment(Qt.AlignRight | Qt.AlignVCenter) layout.addWidget(vl, row, 1) self.version = v = QLineEdit(self.chimerax_version()) v.setReadOnly(True) layout.addWidget(v, row, 2) row += 1 il = QWidget() layout.addWidget(il, row, 2) ilayout = QHBoxLayout(il) ilayout.setContentsMargins(0, 0, 0, 0) self.include_log = ilc = QCheckBox() ilc.setChecked(True) ilayout.addWidget(ilc) ill = QLabel('Include log contents in bug report') ill.setAlignment(Qt.AlignLeft | Qt.AlignVCenter) ilayout.addWidget(ill) ilayout.addStretch(1) row += 1 self.result = rl = QLabel('') rl.setWordWrap(True) layout.addWidget(rl, row, 1, 1, 2) row += 1 # Button row brow = QWidget() blayout = QHBoxLayout() blayout.setContentsMargins(0, 0, 0, 0) blayout.setSpacing(2) brow.setLayout(blayout) layout.addWidget(brow, row, 1, 1, 2) row += 1 blayout.addStretch(1) # Extra space at start of button row. self.submit_button = sb = QPushButton('Submit', brow) sb.clicked.connect(lambda e: self.submit()) blayout.addWidget(sb) self.cancel_button = cb = QPushButton('Cancel', brow) cb.clicked.connect(lambda e: self.cancel()) blayout.addWidget(cb) tw.manage(placement=None) # Don't dock it to main window.
class UpdateTool(ToolInstance): SESSION_ENDURING = True # if SESSION_ENDURING is True, tool instance not deleted at session closure help = "help:user/tools/updates.html" NAME_COLUMN = 0 CURRENT_VERSION_COLUMN = 1 NEW_VERSION_COLUMN = 2 CATEGORY_COLUMN = 3 NUM_COLUMNS = CATEGORY_COLUMN + 1 def __init__(self, session, tool_name, dialog_type=None): if dialog_type is None: dialog_type = DialogType.ALL_AVAILABLE ToolInstance.__init__(self, session, tool_name) from chimerax.ui import MainToolWindow self.tool_window = MainToolWindow(self) parent = self.tool_window.ui_area from PyQt5.QtCore import Qt from PyQt5.QtWidgets import QTreeWidget, QHBoxLayout, QVBoxLayout, QAbstractItemView, \ QPushButton, QLabel, QComboBox layout = QVBoxLayout() parent.setLayout(layout) label = QLabel( "<p>Select the individual bundles you want to install by checking the box." " Then click on the <b>Install</b> button to install them." " The default bundle version is the newest one, but you can select an older version." "<p>When only showing updates, check <b>All</b> to select all of them." ) label.setWordWrap(True) layout.addWidget(label) choice_layout = QHBoxLayout() layout.addLayout(choice_layout) label = QLabel("<b>Show:</b>") choice_layout.addWidget(label) self.choice = QComboBox() choice_layout.addWidget(self.choice) for dt in DialogType: self.choice.addItem(dt.name.replace('_', ' ').title(), dt) self.choice.setCurrentIndex(self.choice.findData(dialog_type)) self.choice.currentIndexChanged.connect(self.new_choice) choice_layout.addStretch() class SizedTreeWidget(QTreeWidget): def sizeHint(self): from PyQt5.QtCore import QSize width = self.header().length() return QSize(width, 200) self.updates = SizedTreeWidget() # self.updates = QTreeWidget(parent) layout.addWidget(self.updates) self.updates.setHeaderLabels([ "Bundle\nName", "Current\nVersion", "Available\nVersion(s)", "Category" ]) self.updates.setSortingEnabled(True) hi = self.updates.headerItem() hi.setTextAlignment(self.NAME_COLUMN, Qt.AlignCenter) hi.setTextAlignment(self.CURRENT_VERSION_COLUMN, Qt.AlignCenter) hi.setTextAlignment(self.NEW_VERSION_COLUMN, Qt.AlignCenter) self.updates.setSelectionBehavior(QAbstractItemView.SelectRows) self.updates.setSelectionMode(QAbstractItemView.ExtendedSelection) self.updates.setEditTriggers(QAbstractItemView.NoEditTriggers) buttons_layout = QHBoxLayout() layout.addLayout(buttons_layout) button = QPushButton("Help") button.clicked.connect(self.help_button) buttons_layout.addWidget(button) buttons_layout.addStretch() button = QPushButton("Install") button.clicked.connect(self.install) buttons_layout.addWidget(button) button = QPushButton("Cancel") button.clicked.connect(self.cancel) buttons_layout.addWidget(button) self._fill_updates() self.tool_window.fill_context_menu = self.fill_context_menu self.tool_window.manage(placement=None) def help_button(self): from chimerax.help_viewer import show_url show_url(self.session, self.help, new_tab=True) def cancel(self): self.session.ui.main_window.close_request(self.tool_window) def fill_context_menu(self, menu, x, y): from PyQt5.QtWidgets import QAction settings_action = QAction("Settings...", menu) settings_action.triggered.connect(lambda arg: self.show_settings()) menu.addAction(settings_action) def show_settings(self): self.session.ui.main_window.show_settings('Toolshed') def _fill_updates(self): from PyQt5.QtCore import Qt from PyQt5.QtWidgets import QTreeWidgetItem, QComboBox from packaging.version import Version session = self.session toolshed = session.toolshed self.actions = [] info = toolshed.bundle_info(session.logger, installed=False, available=True) dialog_type = self.choice.currentData() new_bundles = {} last_bundle_name = None installed_version = "" for available in info: if last_bundle_name is None or available.name != last_bundle_name: last_bundle_name = available.name installed_bi = toolshed.find_bundle(last_bundle_name, session.logger) if installed_bi is not None: installed_version = Version(installed_bi.version) else: installed_version = "" if dialog_type != DialogType.ALL_AVAILABLE: continue elif not installed_version and dialog_type != DialogType.ALL_AVAILABLE: continue new_version = Version(available.version) if dialog_type == DialogType.UPDATES_ONLY: if new_version <= installed_version: continue data = new_bundles.setdefault( last_bundle_name, ([], installed_version, available.synopsis, available.categories[0])) data[0].append(new_version) self.updates.clear() if not new_bundles: return if dialog_type != DialogType.UPDATES_ONLY: self.all_items = all_items = self.updates.invisibleRootItem() else: self.all_items = all_items = QTreeWidgetItem() all_items.setText(0, "All") all_items.setFlags(Qt.ItemIsEnabled | Qt.ItemIsUserCheckable | Qt.ItemIsAutoTristate) all_items.setCheckState(self.NAME_COLUMN, Qt.Unchecked) self.updates.addTopLevelItem(all_items) self.updates.expandItem(all_items) flags = Qt.ItemIsEnabled | Qt.ItemIsUserCheckable | Qt.ItemIsSelectable | Qt.ItemNeverHasChildren for bundle_name in new_bundles: new_versions, installed_version, synopsis, category = new_bundles[ bundle_name] item = QTreeWidgetItem(all_items) # Name column item.setData(self.NAME_COLUMN, Qt.UserRole, bundle_name) if False: # TODO: need delagate to show Qt Rich Text item.setText(self.NAME_COLUMN, toolshed.bundle_link(bundle_name)) else: if bundle_name.startswith("ChimeraX-"): bundle_name = bundle_name[len("ChimeraX-"):] item.setText(self.NAME_COLUMN, bundle_name) item.setFlags(flags) item.setToolTip(self.NAME_COLUMN, synopsis) item.setCheckState(self.NAME_COLUMN, Qt.Unchecked) # Current version column item.setText(self.CURRENT_VERSION_COLUMN, str(installed_version)) item.setTextAlignment(self.CURRENT_VERSION_COLUMN, Qt.AlignCenter) # New version column b = QComboBox() b.addItems(str(v) for v in sorted(new_versions, reverse=True)) # TODO: set background color to same as other columns self.updates.setItemWidget(item, self.NEW_VERSION_COLUMN, b) # Category column item.setText(self.CATEGORY_COLUMN, category) self.updates.sortItems(self.NAME_COLUMN, Qt.AscendingOrder) for column in range(self.NUM_COLUMNS): self.updates.resizeColumnToContents(column) def new_choice(self): self._fill_updates() def install(self): from PyQt5.QtCore import Qt toolshed = self.session.toolshed logger = self.session.logger all_items = self.all_items updating = [] for i in range(all_items.childCount()): item = all_items.child(i) if item.checkState(self.NAME_COLUMN) == Qt.Unchecked: continue bundle_name = item.data(self.NAME_COLUMN, Qt.UserRole) w = self.updates.itemWidget(item, self.NEW_VERSION_COLUMN) version = w.currentText() bi = toolshed.find_bundle(bundle_name, logger, version=version, installed=False) updating.append(bi) from . import _install_bundle _install_bundle(toolshed, updating, logger, reinstall=True, session=self.session) self.cancel()
class LibAdd(ToolInstance): help = "https://github.com/QChASM/SEQCROW/wiki/Add-to-Personal-Library-Tool" SESSION_ENDURING = False SESSION_SAVE = False def __init__(self, session, name): super().__init__(session, name) from chimerax.ui import MainToolWindow self.tool_window = MainToolWindow(self) self.key_atomspec = [] self._build_ui() def _build_ui(self): layout = QGridLayout() library_tabs = QTabWidget() #ligand tab ligand_tab = QWidget() ligand_layout = QFormLayout(ligand_tab) self.ligand_name = QLineEdit() self.ligand_name.setText("") self.ligand_name.setPlaceholderText("leave blank to preview") self.ligand_name.setToolTip( "name of ligand you are adding to your ligand library\nleave blank to open a new model with just the ligand" ) ligand_layout.addRow("ligand name:", self.ligand_name) ligand_key_atoms = QPushButton("set key atoms to current selection") ligand_key_atoms.clicked.connect(self.update_key_atoms) ligand_key_atoms.setToolTip( "the current selection will be the key atoms for the ligand\nleave blank to automatically determine key atoms" ) ligand_layout.addRow(ligand_key_atoms) libadd_ligand = QPushButton("add current selection to library") libadd_ligand.clicked.connect(self.libadd_ligand) ligand_layout.addRow(libadd_ligand) #substituent tab sub_tab = QWidget() sub_layout = QFormLayout(sub_tab) self.sub_name = QLineEdit() self.sub_name.setText("") self.sub_name.setPlaceholderText("leave blank to preview") self.sub_name.setToolTip( "name of substituent you are adding to your substituent library\nleave blank to open a new model with just the substituent" ) sub_layout.addRow("substituent name:", self.sub_name) self.sub_confs = QSpinBox() self.sub_confs.setMinimum(1) sub_layout.addRow("number of conformers:", self.sub_confs) self.sub_angle = QSpinBox() self.sub_angle.setRange(0, 180) self.sub_angle.setSingleStep(30) sub_layout.addRow("angle between conformers:", self.sub_angle) libadd_sub = QPushButton("add current selection to library") libadd_sub.clicked.connect(self.libadd_substituent) sub_layout.addRow(libadd_sub) #ring tab ring_tab = QWidget() ring_layout = QFormLayout(ring_tab) self.ring_name = QLineEdit() self.ring_name.setText("") self.ring_name.setPlaceholderText("leave blank to preview") self.ring_name.setToolTip( "name of ring you are adding to your ring library\nleave blank to open a new model with just the ring" ) ring_layout.addRow("ring name:", self.ring_name) libadd_ring = QPushButton("add ring with selected walk to library") libadd_ring.clicked.connect(self.libadd_ring) ring_layout.addRow(libadd_ring) library_tabs.addTab(sub_tab, "substituent") library_tabs.addTab(ring_tab, "ring") library_tabs.addTab(ligand_tab, "ligand") self.library_tabs = library_tabs layout.addWidget(library_tabs) whats_this = QLabel() whats_this.setText( "<a href=\"req\" style=\"text-decoration: none;\">what's this?</a>" ) whats_this.setTextFormat(Qt.RichText) whats_this.setTextInteractionFlags(Qt.TextBrowserInteraction) whats_this.linkActivated.connect(self.open_link) whats_this.setToolTip( "click for more information about AaronTools libraries") layout.addWidget(whats_this) self.tool_window.ui_area.setLayout(layout) self.tool_window.manage(None) def update_key_atoms(self): selection = selected_atoms(self.session) if not selection.single_structure: raise RuntimeError("selected atoms must be on the same model") else: self.key_atomspec = selection self.tool_window.status("key atoms set to %s" % " ".join(atom.atomspec for atom in selection)) def libadd_ligand(self): """add ligand to library or open it in a new model""" selection = selected_atoms(self.session) if not selection.single_structure: raise RuntimeError("selected atoms must be on the same model") rescol = ResidueCollection(selection[0].structure) ligand_atoms = [ atom for atom in rescol.atoms if atom.chix_atom in selection ] key_chix_atoms = [ atom for atom in self.key_atomspec if not atom.deleted ] if len(key_chix_atoms) < 1: key_atoms = set([]) for atom in ligand_atoms: for atom2 in atom.connected: if atom2 not in ligand_atoms: key_atoms.add(atom) else: key_atoms = rescol.find( [AtomSpec(atom.atomspec) for atom in key_chix_atoms]) if len(key_atoms) < 1: raise RuntimeError("no key atoms could be determined") lig_name = self.ligand_name.text() ligand = Component(ligand_atoms, name=lig_name, key_atoms=key_atoms) ligand.comment = "K:%s" % ",".join( [str(ligand.atoms.index(atom) + 1) for atom in key_atoms]) if len(lig_name) == 0: chimerax_ligand = ResidueCollection(ligand).get_chimera( self.session) chimerax_ligand.name = "ligand preview" self.session.models.add([chimerax_ligand]) bild_obj = key_atom_highlight(ligand, [0.2, 0.5, 0.8, 0.5], self.session) self.session.models.add(bild_obj, parent=chimerax_ligand) else: check_aaronlib_dir() filename = os.path.join(AARONLIB, "Ligands", lig_name + ".xyz") if os.path.exists(filename): exists_warning = QMessageBox() exists_warning.setIcon(QMessageBox.Warning) exists_warning.setText( "%s already exists.\nWould you like to overwrite?" % filename) exists_warning.setStandardButtons(QMessageBox.Yes | QMessageBox.No) rv = exists_warning.exec_() if rv == QMessageBox.Yes: ligand.write(outfile=filename) self.tool_window.status("%s added to ligand library" % lig_name) else: self.tool_window.status( "%s has not been added to ligand library" % lig_name) else: ligand.write(outfile=filename) self.tool_window.status("%s added to ligand library" % lig_name) def libadd_ring(self): """add ring to library or open it in a new model""" selection = self.session.seqcrow_ordered_selection_manager.selection if not selection.single_structure: raise RuntimeError("selected atoms must be on the same model") rescol = ResidueCollection(selection[0].structure) walk_atoms = rescol.find( [AtomSpec(atom.atomspec) for atom in selection]) if len(walk_atoms) < 1: raise RuntimeError("no walk direction could be determined") ring_name = self.ring_name.text() ring = Ring(rescol, name=ring_name, end=walk_atoms) ring.comment = "E:%s" % ",".join( [str(rescol.atoms.index(atom) + 1) for atom in walk_atoms]) if len(ring_name) == 0: chimerax_ring = ResidueCollection(ring).get_chimera(self.session) chimerax_ring.name = "ring preview" self.session.models.add([chimerax_ring]) bild_obj = show_walk_highlight(ring, chimerax_ring, [0.9, 0.4, 0.3, 0.9], self.session) self.session.models.add(bild_obj, parent=chimerax_ring) else: check_aaronlib_dir() filename = os.path.join(AARONLIB, "Rings", ring_name + ".xyz") if os.path.exists(filename): exists_warning = QMessageBox() exists_warning.setIcon(QMessageBox.Warning) exists_warning.setText( "%s already exists.\nWould you like to overwrite?" % filename) exists_warning.setStandardButtons(QMessageBox.Yes | QMessageBox.No) rv = exists_warning.exec_() if rv == QMessageBox.Yes: ring.write(outfile=filename) self.tool_window.status("%s added to ring library" % ring_name) else: self.tool_window.status( "%s has not been added to ring library" % ring_name) else: ring.write(outfile=filename) self.tool_window.status("%s added to ring library" % ring_name) def libadd_substituent(self): """add ligand to library or open it in a new model""" selection = selected_atoms(self.session) if not selection.single_structure: raise RuntimeError("selected atoms must be on the same model") residues = [] for atom in selection: if atom.residue not in residues: residues.append(atom.residue) rescol = ResidueCollection(selection[0].structure, convert_residues=residues) substituent_atoms = [ atom for atom in rescol.atoms if atom.chix_atom in selection ] start = None avoid = None for atom in substituent_atoms: for atom2 in atom.connected: if atom2 not in substituent_atoms: if start is None: start = atom avoid = atom2 else: raise RuntimeError( "substituent must only have one connection to the molecule" ) if start is None: raise RuntimeError( "substituent is not connected to a larger molecule") substituent_atoms.remove(start) substituent_atoms.insert(0, start) sub_name = self.sub_name.text() confs = self.sub_confs.value() angle = self.sub_angle.value() comment = "CF:%i,%i" % (confs, angle) if len(sub_name) == 0: sub = Substituent(substituent_atoms, name="test", conf_num=confs, conf_angle=angle) else: sub = Substituent(substituent_atoms, name=sub_name, conf_num=confs, conf_angle=angle) sub.comment = comment #align substituent bond to x axis sub.coord_shift(-avoid.coords) x_axis = np.array([1., 0., 0.]) n = np.linalg.norm(start.coords) vb = start.coords / n d = np.linalg.norm(vb - x_axis) theta = np.arccos((d**2 - 2) / -2) vx = np.cross(vb, x_axis) sub.rotate(vx, theta) add = False if len(sub_name) == 0: chimerax_sub = ResidueCollection(sub).get_chimera(self.session) chimerax_sub.name = "substituent preview" self.session.models.add([chimerax_sub]) bild_obj = ghost_connection_highlight( sub, [0.60784, 0.145098, 0.70196, 0.5], self.session) self.session.models.add(bild_obj, parent=chimerax_sub) else: check_aaronlib_dir() filename = os.path.join(AARONLIB, "Subs", sub_name + ".xyz") if os.path.exists(filename): exists_warning = QMessageBox() exists_warning.setIcon(QMessageBox.Warning) exists_warning.setText( "%s already exists.\nWould you like to overwrite?" % filename) exists_warning.setStandardButtons(QMessageBox.Yes | QMessageBox.No) rv = exists_warning.exec_() if rv == QMessageBox.Yes: add = True else: self.tool_window.status( "%s has not been added to substituent library" % sub_name) else: add = True if add: sub.write(outfile=filename) self.tool_window.status("%s added to substituent library" % sub_name) register_selectors(self.session.logger, sub_name) if self.session.ui.is_gui: if (sub_name not in ELEMENTS and sub_name[0].isalpha() and (len(sub_name) > 1 and not any(not (c.isalnum() or c in "+-") for c in sub_name[1:]))): add_submenu = self.session.ui.main_window.add_select_submenu add_selector = self.session.ui.main_window.add_menu_selector substituent_menu = add_submenu(['Che&mistry'], 'Substituents') add_selector(substituent_menu, sub_name, sub_name) def display_help(self): """Show the help for this tool in the help viewer.""" from chimerax.core.commands import run run(self.session, 'open %s' % self.help if self.help is not None else "") def open_link(self, *args): if self.library_tabs.currentIndex() == 0: link = "https://github.com/QChASM/AaronTools.py/wiki/AaronTools-Libraries#substituents" elif self.library_tabs.currentIndex() == 1: link = "https://github.com/QChASM/AaronTools.py/wiki/AaronTools-Libraries#rings" elif self.library_tabs.currentIndex() == 2: link = "https://github.com/QChASM/AaronTools.py/wiki/AaronTools-Libraries#ligands" run(self.session, "open %s" % link)
def __init__(self, session, tool_name): self._default_color = (0, 0, 204, 255) self._default_box_color = (0, 255, 0, 255) ToolInstance.__init__(self, session, tool_name) self.display_name = 'Measure and Color Blobs' from chimerax.ui import MainToolWindow tw = MainToolWindow(self) self.tool_window = tw parent = tw.ui_area from PyQt5.QtWidgets import QVBoxLayout, QHBoxLayout, QFrame, QCheckBox, QLabel, QPushButton, QSizePolicy from PyQt5.QtCore import Qt layout = QVBoxLayout(parent) layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) parent.setLayout(layout) cf = QFrame(parent) # cf.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum) layout.addWidget(cf) clayout = QHBoxLayout(cf) clayout.setContentsMargins(0, 0, 0, 0) clayout.setSpacing(10) self._color_blob = cb = QCheckBox('Color blob', cf) cb.setCheckState(Qt.Checked) clayout.addWidget(cb) from chimerax.ui.widgets import ColorButton self._blob_color = cbut = ColorButton(cf, max_size=(16, 16)) cbut.color = self._default_color clayout.addWidget(cbut) clayout.addSpacing(10) self._change_color = cc = QCheckBox('Change color automatically', cf) cc.setCheckState(Qt.Checked) clayout.addWidget(cc) clayout.addStretch(1) # Extra space at end af = QFrame(parent) # af.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum) layout.addWidget(af) aflayout = QHBoxLayout(af) aflayout.setContentsMargins(0, 0, 0, 0) aflayout.setSpacing(10) self._show_box = bb = QCheckBox('Show principal axes box', af) bb.setCheckState(Qt.Checked) aflayout.addWidget(bb) self._box_color = bc = ColorButton(af, max_size=(16, 16)) bc.color = self._default_box_color aflayout.addWidget(bc) aflayout.addSpacing(10) eb = QPushButton('Erase boxes', af) eb.clicked.connect(self._erase_boxes) aflayout.addWidget(eb) aflayout.addStretch(1) # Extra space at end self._message_label = ml = QLabel(parent) layout.addWidget(ml) layout.addStretch(1) # Extra space at end tw.manage(placement="side")
class LigandSterimol(ToolInstance): help = "https://github.com/QChASM/SEQCROW/wiki/Ligand-Sterimol-Tool" SESSION_ENDURING = False SESSION_SAVE = False def __init__(self, session, name): super().__init__(session, name) from chimerax.ui import MainToolWindow self.tool_window = MainToolWindow(self) self.settings = _SterimolSettings(self.session, name) self._build_ui() def _build_ui(self): layout = QFormLayout() self.radii_option = QComboBox() self.radii_option.addItems(["Bondi", "UMN"]) ndx = self.radii_option.findText(self.settings.radii, Qt.MatchExactly) self.radii_option.setCurrentIndex(ndx) layout.addRow("radii:", self.radii_option) self.L_option = QComboBox() self.L_option.addItems([ "to centroid of coordinating atoms", "bisect angle between coordinating atoms" ]) ndx = self.L_option.findText(self.settings.L_option, Qt.MatchExactly) self.L_option.setCurrentIndex(ndx) layout.addRow("L axis:", self.L_option) self.sterimol2vec = QGroupBox("Sterimol2Vec") sterimol2vec_layout = QFormLayout(self.sterimol2vec) self.at_L = QDoubleSpinBox() self.at_L.setRange(-10, 30) self.at_L.setDecimals(2) self.at_L.setSingleStep(0.25) self.at_L.setValue(self.settings.at_L) sterimol2vec_layout.addRow("L value:", self.at_L) layout.addRow(self.sterimol2vec) self.sterimol2vec.setCheckable(True) self.sterimol2vec.toggled.connect(lambda x: self.at_L.setEnabled(x)) self.sterimol2vec.setChecked(self.settings.sterimol2vec) self.display_vectors = QCheckBox() self.display_vectors.setChecked(self.settings.display_vectors) layout.addRow("show vectors:", self.display_vectors) self.display_radii = QCheckBox() self.display_radii.setChecked(self.settings.display_radii) layout.addRow("show radii:", self.display_radii) calc_sterimol_button = QPushButton( "calculate parameters for selected ligands") calc_sterimol_button.clicked.connect(self.calc_sterimol) layout.addRow(calc_sterimol_button) self.calc_sterimol_button = calc_sterimol_button remove_sterimol_button = QPushButton("remove Sterimol visualizations") remove_sterimol_button.clicked.connect(self.del_sterimol) layout.addRow(remove_sterimol_button) self.remove_sterimol_button = remove_sterimol_button self.table = QTableWidget() self.table.setColumnCount(8) self.table.setHorizontalHeaderLabels([ 'model', 'coord. atoms', 'B\u2081', 'B\u2082', 'B\u2083', 'B\u2084', 'B\u2085', 'L', ]) self.table.setSelectionBehavior(QTableWidget.SelectRows) self.table.setEditTriggers(QTableWidget.NoEditTriggers) self.table.resizeColumnToContents(0) self.table.resizeColumnToContents(1) self.table.resizeColumnToContents(2) self.table.resizeColumnToContents(3) self.table.resizeColumnToContents(4) self.table.resizeColumnToContents(5) self.table.resizeColumnToContents(6) self.table.resizeColumnToContents(7) self.table.horizontalHeader().setSectionResizeMode( 2, QHeaderView.Stretch) self.table.horizontalHeader().setSectionResizeMode( 3, QHeaderView.Stretch) self.table.horizontalHeader().setSectionResizeMode( 4, QHeaderView.Stretch) self.table.horizontalHeader().setSectionResizeMode( 5, QHeaderView.Stretch) self.table.horizontalHeader().setSectionResizeMode( 6, QHeaderView.Stretch) self.table.horizontalHeader().setSectionResizeMode( 7, QHeaderView.Stretch) layout.addRow(self.table) menu = QMenuBar() export = menu.addMenu("&Export") clear = QAction("Clear data table", self.tool_window.ui_area) clear.triggered.connect(self.clear_table) export.addAction(clear) copy = QAction("&Copy CSV to clipboard", self.tool_window.ui_area) copy.triggered.connect(self.copy_csv) shortcut = QKeySequence(Qt.CTRL + Qt.Key_C) copy.setShortcut(shortcut) export.addAction(copy) save = QAction("&Save CSV...", self.tool_window.ui_area) save.triggered.connect(self.save_csv) #this shortcut interferes with main window's save shortcut #I've tried different shortcut contexts to no avail #thanks Qt... #shortcut = QKeySequence(Qt.CTRL + Qt.Key_S) #save.setShortcut(shortcut) #save.setShortcutContext(Qt.WidgetShortcut) export.addAction(save) delimiter = export.addMenu("Delimiter") comma = QAction("comma", self.tool_window.ui_area, checkable=True) comma.setChecked(self.settings.delimiter == "comma") comma.triggered.connect(lambda *args, delim="comma": self.settings. __setattr__("delimiter", delim)) delimiter.addAction(comma) tab = QAction("tab", self.tool_window.ui_area, checkable=True) tab.setChecked(self.settings.delimiter == "tab") tab.triggered.connect(lambda *args, delim="tab": self.settings. __setattr__("delimiter", delim)) delimiter.addAction(tab) space = QAction("space", self.tool_window.ui_area, checkable=True) space.setChecked(self.settings.delimiter == "space") space.triggered.connect(lambda *args, delim="space": self.settings. __setattr__("delimiter", delim)) delimiter.addAction(space) semicolon = QAction("semicolon", self.tool_window.ui_area, checkable=True) semicolon.setChecked(self.settings.delimiter == "semicolon") semicolon.triggered.connect(lambda *args, delim="semicolon": self. settings.__setattr__("delimiter", delim)) delimiter.addAction(semicolon) add_header = QAction("&Include CSV header", self.tool_window.ui_area, checkable=True) add_header.setChecked(self.settings.include_header) add_header.triggered.connect(self.header_check) export.addAction(add_header) comma.triggered.connect( lambda *args, action=tab: action.setChecked(False)) comma.triggered.connect( lambda *args, action=space: action.setChecked(False)) comma.triggered.connect( lambda *args, action=semicolon: action.setChecked(False)) tab.triggered.connect( lambda *args, action=comma: action.setChecked(False)) tab.triggered.connect( lambda *args, action=space: action.setChecked(False)) tab.triggered.connect( lambda *args, action=semicolon: action.setChecked(False)) space.triggered.connect( lambda *args, action=comma: action.setChecked(False)) space.triggered.connect( lambda *args, action=tab: action.setChecked(False)) space.triggered.connect( lambda *args, action=semicolon: action.setChecked(False)) semicolon.triggered.connect( lambda *args, action=comma: action.setChecked(False)) semicolon.triggered.connect( lambda *args, action=tab: action.setChecked(False)) semicolon.triggered.connect( lambda *args, action=space: action.setChecked(False)) menu.setNativeMenuBar(False) self._menu = menu layout.setMenuBar(menu) menu.setVisible(True) self.tool_window.ui_area.setLayout(layout) self.tool_window.manage(None) def clear_table(self): are_you_sure = QMessageBox.question( None, "Clear table?", "Are you sure you want to clear the data table?", ) if are_you_sure != QMessageBox.Yes: return self.table.setRowCount(0) def calc_sterimol(self, *args): self.settings.radii = self.radii_option.currentText() self.settings.display_radii = self.display_radii.checkState( ) == Qt.Checked self.settings.display_vectors = self.display_vectors.checkState( ) == Qt.Checked self.settings.at_L = self.at_L.value() self.settings.sterimol2vec = self.sterimol2vec.isChecked() self.settings.L_option = self.L_option.currentText() targets, neighbors, datas = sterimol_cmd( self.session, selected_atoms(self.session), radii=self.radii_option.currentText(), showVectors=self.display_vectors.checkState() == Qt.Checked, showRadii=self.display_radii.checkState() == Qt.Checked, return_values=True, at_L=self.at_L.value() if self.sterimol2vec.isChecked() else None, bisect_L=self.L_option.currentText() == "bisect angle between coordinating atoms", ) if len(targets) == 0: return if self.settings.delimiter == "comma": delim = "," elif self.settings.delimiter == "space": delim = " " elif self.settings.delimiter == "tab": delim = "\t" elif self.settings.delimiter == "semicolon": delim = ";" # self.table.setRowCount(0) for t, b, data in zip(targets, neighbors, datas): row = self.table.rowCount() self.table.insertRow(row) targ = QTableWidgetItem() targ.setData(Qt.DisplayRole, t) self.table.setItem(row, 0, targ) neigh = QTableWidgetItem() neigh.setData(Qt.DisplayRole, delim.join(b)) self.table.setItem(row, 1, neigh) l = np.linalg.norm(data["L"][1] - data["L"][0]) b1 = np.linalg.norm(data["B1"][1] - data["B1"][0]) b2 = np.linalg.norm(data["B2"][1] - data["B2"][0]) b3 = np.linalg.norm(data["B3"][1] - data["B3"][0]) b4 = np.linalg.norm(data["B4"][1] - data["B4"][0]) b5 = np.linalg.norm(data["B5"][1] - data["B5"][0]) li = QTableWidgetItem() li.setData(Qt.DisplayRole, "%.2f" % l) self.table.setItem(row, 7, li) b1i = QTableWidgetItem() b1i.setData(Qt.DisplayRole, "%.2f" % b1) self.table.setItem(row, 2, b1i) b2i = QTableWidgetItem() b2i.setData(Qt.DisplayRole, "%.2f" % b2) self.table.setItem(row, 3, b2i) b3i = QTableWidgetItem() b3i.setData(Qt.DisplayRole, "%.2f" % b3) self.table.setItem(row, 4, b3i) b4i = QTableWidgetItem() b4i.setData(Qt.DisplayRole, "%.2f" % b4) self.table.setItem(row, 5, b4i) b5i = QTableWidgetItem() b5i.setData(Qt.DisplayRole, "%.2f" % b5) self.table.setItem(row, 6, b5i) for i in range(0, 7): if i == 1: continue self.table.resizeColumnToContents(i) def header_check(self, state): """user has [un]checked the 'include header' option on the menu""" if state: self.settings.include_header = True else: self.settings.include_header = False def get_csv(self): if self.settings.delimiter == "comma": delim = "," elif self.settings.delimiter == "space": delim = " " elif self.settings.delimiter == "tab": delim = "\t" elif self.settings.delimiter == "semicolon": delim = ";" if self.settings.include_header: s = delim.join([ "substituent_atom", "bonded_atom", "B1", "B2", "B3", "B4", "B5", "L" ]) s += "\n" else: s = "" for i in range(0, self.table.rowCount()): s += delim.join([ item.data(Qt.DisplayRole) for item in [self.table.item(i, j) for j in range(0, 8)] ]) s += "\n" return s def copy_csv(self): app = QApplication.instance() clipboard = app.clipboard() csv = self.get_csv() clipboard.setText(csv) self.session.logger.status("copied to clipboard") def save_csv(self): """save data on current tab to CSV file""" filename, _ = QFileDialog.getSaveFileName(filter="CSV Files (*.csv)") if filename: s = self.get_csv() with open(filename, 'w') as f: f.write(s.strip()) self.session.logger.status("saved to %s" % filename) def del_sterimol(self): for model in self.session.models.list(type=Generic3DModel): if model.name.startswith("Sterimol"): model.delete() def display_help(self): """Show the help for this tool in the help viewer.""" from chimerax.core.commands import run run(self.session, 'open %s' % self.help if self.help is not None else "")
class HelpUI(ToolInstance): # do not close when opening session (especially if web page asked to open session) SESSION_ENDURING = True help = "help:user/tools/helpviewer.html" def __init__(self, session): tool_name = "Help Viewer" ToolInstance.__init__(self, session, tool_name) self._pending_downloads = [] from chimerax.ui import MainToolWindow self.tool_window = MainToolWindow(self) parent = self.tool_window.ui_area # UI content code from PyQt5.QtWidgets import QToolBar, QVBoxLayout, QAction, QLineEdit, QTabWidget, QShortcut, QStatusBar from PyQt5.QtGui import QIcon from PyQt5.QtCore import Qt shortcuts = ( (Qt.CTRL + Qt.Key_0, self.page_reset_zoom), (Qt.CTRL + Qt.Key_T, lambda: self.create_tab(empty=True)), (Qt.CTRL + Qt.Key_W, self.close_current_tab), (Qt.CTRL + Qt.Key_Tab, lambda: self.cycle_tab(1)), (Qt.CTRL + Qt.SHIFT + Qt.Key_Tab, lambda: self.cycle_tab(-1)), (Qt.CTRL + Qt.Key_1, lambda: self.tab_n(0)), (Qt.CTRL + Qt.Key_2, lambda: self.tab_n(1)), (Qt.CTRL + Qt.Key_3, lambda: self.tab_n(2)), (Qt.CTRL + Qt.Key_4, lambda: self.tab_n(3)), (Qt.CTRL + Qt.Key_5, lambda: self.tab_n(4)), (Qt.CTRL + Qt.Key_6, lambda: self.tab_n(5)), (Qt.CTRL + Qt.Key_7, lambda: self.tab_n(6)), (Qt.CTRL + Qt.Key_8, lambda: self.tab_n(7)), (Qt.CTRL + Qt.Key_9, lambda: self.tab_n(-1)), ) for shortcut, callback in shortcuts: sc = QShortcut(shortcut, parent) sc.activated.connect(callback) self.toolbar = tb = QToolBar() # tb.setToolButtonStyle(Qt.ToolButtonTextUnderIcon) layout = QVBoxLayout() layout.setContentsMargins(0, 1, 0, 0) layout.addWidget(tb) parent.setLayout(layout) import os.path icon_dir = os.path.dirname(__file__) # attribute, text, tool tip, callback, shortcut(s), enabled buttons = ( ("back", "Back", "Back to previous page", self.page_back, Qt.Key_Back, False), ("forward", "Forward", "Next page", self.page_forward, Qt.Key_Forward, False), ("reload", "Reload", "Reload page", self.page_reload, Qt.Key_Reload, True), ("new_tab", "New Tab", "New Tab", lambda: self.create_tab(empty=True), Qt.Key_Reload, True), ("zoom_in", "Zoom in", "Zoom in", self.page_zoom_in, [Qt.CTRL + Qt.Key_Plus, Qt.Key_ZoomIn, Qt.CTRL + Qt.Key_Equal], True), ("zoom_out", "Zoom out", "Zoom out", self.page_zoom_out, [Qt.CTRL + Qt.Key_Minus, Qt.Key_ZoomOut], True), ("home", "Home", "Home page", self.page_home, Qt.Key_HomePage, True), (None, None, None, None, None, None), ("search", "Search", "Search in page", self.page_search, Qt.Key_Search, True), ) for attribute, text, tooltip, callback, shortcut, enabled in buttons: if attribute is None: tb.addSeparator() continue icon_path = os.path.join(icon_dir, "%s.svg" % attribute) setattr(self, attribute, QAction(QIcon(icon_path), text, tb)) a = getattr(self, attribute) a.setToolTip(tooltip) a.triggered.connect(callback) if shortcut: if isinstance(shortcut, list): a.setShortcuts(shortcut) else: a.setShortcut(shortcut) a.setEnabled(enabled) tb.addAction(a) self.url = QLineEdit() self.url.setPlaceholderText("url") self.url.setClearButtonEnabled(True) self.url.returnPressed.connect(self.go_to) tb.insertWidget(self.reload, self.url) self.search_terms = QLineEdit() self.search_terms.setClearButtonEnabled(True) self.search_terms.setPlaceholderText("search in page") self.search_terms.setMaximumWidth(200) self.search_terms.returnPressed.connect(self.page_search) tb.addWidget(self.search_terms) self.tabs = QTabWidget(parent) self.tabs.setTabsClosable(True) self.tabs.setUsesScrollButtons(True) self.tabs.setTabBarAutoHide(True) self.tabs.setDocumentMode(False) self.tabs.currentChanged.connect(self.tab_changed) self.tabs.tabCloseRequested.connect(self.close_tab) layout.addWidget(self.tabs) self.status_bar = QStatusBar() layout.addWidget(self.status_bar) self.tool_window.manage(placement=None) self.profile = create_chimerax_profile( self.tabs, interceptor=self.intercept, download=self.download_requested) def status(self, message): self.status_bar.showMessage(message, 2000) def create_tab(self, *, empty=False, background=False): w = _HelpWebView(self.session, self, profile=self.profile) self.tabs.addTab(w, "New Tab") if empty: from chimerax import app_dirs self.tool_window.title = app_dirs.appname from PyQt5.QtCore import Qt self.url.setFocus(Qt.ShortcutFocusReason) if not background: self.tabs.setCurrentWidget(w) p = w.page() p.loadFinished.connect(lambda okay, w=w: self.page_loaded(w, okay)) p.urlChanged.connect(lambda url, w=w: self.url_changed(w, url)) p.titleChanged.connect(lambda title, w=w: self.title_changed(w, title)) p.linkHovered.connect(self.link_hovered) p.authenticationRequired.connect(self.authorize) # TODO? p.iconChanged.connect(....) # TODO? p.iconUrlChanged.connect(....) # TODO? p.loadProgress.connect(....) # TODO? p.loadStarted.connect(....) # TODO? p.renderProcessTerminated.connect(....) # TODO? p.selectionChanged.connect(....) # TODO? p.windowCloseRequested.connect(....) return w def authorize(self, requestUrl, auth): from PyQt5.QtWidgets import QDialog, QGridLayout, QLineEdit, QLabel, QPushButton from PyQt5.QtCore import Qt class PasswordDialog(QDialog): def __init__(self, requestUrl, auth, parent=None): super().__init__(parent) self.setWindowTitle("ChimeraX: Authentication Required") self.setModal(True) self.auth = auth url = requestUrl.url() key = QLabel("\N{KEY}") font = key.font() font.setPointSize(2 * font.pointSize()) key.setFont(font) self.info = QLabel( f'{url} is requesting your username and password. The site says: "{auth.realm()}"' ) self.info.setWordWrap(True) user_name = QLabel("User name:") self.user_name = QLineEdit(self) password = QLabel("Password:"******"item" is an instance of QWebEngineDownloadItem # print("HelpUI.download_requested", item) import os url_file = item.url().fileName() base, extension = os.path.splitext(url_file) # print("HelpUI.download_requested connect", item.mimeType(), extension) # Normally, we would look at the download type or MIME type, # but since neither one is set by the server, we look at the # download extension instead if extension == ".whl": if not base.endswith("x86_64"): # Since the file name encodes the package name and version # number, we make sure that we are using the right name # instead of whatever QWebEngine may want to use. # Remove _# which may be present if bundle author submitted # the same version of the bundle multiple times. parts = base.rsplit('_', 1) if len(parts) == 2 and parts[1].isdigit(): url_file = parts[0] + extension file_path = os.path.join(os.path.dirname(item.path()), url_file) import pkg_resources py_env = pkg_resources.Environment() dist = pkg_resources.Distribution.from_filename(file_path) if not py_env.can_add(dist): raise ValueError("unsupported wheel platform") item.setPath(file_path) # print("HelpUI.download_requested clean", file_path) try: # Guarantee that file name is available os.remove(file_path) except OSError: pass self._pending_downloads.append(item) self.session.logger.info("Downloading bundle %s" % url_file) item.finished.connect(self.download_finished) else: from PyQt5.QtWidgets import QFileDialog path, filt = QFileDialog.getSaveFileName(directory=item.path()) if not path: return self.session.logger.info("Downloading file %s" % url_file) item.setPath(path) # print("HelpUI.download_requested accept", file_path) item.accept() def download_finished(self, *args, **kw): # print("HelpUI.download_finished", args, kw) finished = [] pending = [] for item in self._pending_downloads: if not item.isFinished(): pending.append(item) else: finished.append(item) self._pending_downloads = pending import pkginfo from chimerax.ui.ask import ask for item in finished: item.finished.disconnect() filename = item.path() try: w = pkginfo.Wheel(filename) except Exception as e: self.session.logger.info("Error parsing %s: %s" % (filename, str(e))) self.session.logger.info("File saved as %s" % filename) continue if not _installable(w, self.session.logger): self.session.logger.info("Bundle saved as %s" % filename) continue how = ask(self.session, "Install %s %s (file %s)?" % (w.name, w.version, filename), ["install", "cancel"], title="Toolshed") if how == "cancel": self.session.logger.info("Bundle installation canceled") continue self.session.toolshed.install_bundle(filename, self.session.logger, per_user=True, session=self.session) def show(self, url, *, new_tab=False, html=None): from urllib.parse import urlparse, urlunparse parts = urlparse(url) if not parts.scheme: parts = list(parts) parts[0] = "http" url = urlunparse(parts) # canonicalize if new_tab or self.tabs.count() == 0: w = self.create_tab() else: w = self.tabs.currentWidget() from PyQt5.QtCore import QUrl if html: w.setHtml(html, QUrl(url)) else: w.setUrl(QUrl(url)) self.display(True) def go_to(self): self.show(self.url.text()) def page_back(self, checked): w = self.tabs.currentWidget() if w is None: return w.history().back() def page_forward(self, checked): w = self.tabs.currentWidget() if w is None: return w.history().forward() def page_home(self, checked): w = self.tabs.currentWidget() if w is None: return history = w.history() hi = history.itemAt(0) self.show(_qurl2text(hi.url())) def page_zoom_in(self): w = self.tabs.currentWidget() if w is None: return w.setZoomFactor(1.25 * w.zoomFactor()) def page_zoom_out(self): w = self.tabs.currentWidget() if w is None: return w.setZoomFactor(0.8 * w.zoomFactor()) def page_reset_zoom(self): w = self.tabs.currentWidget() if w is None: return w.setZoomFactor(1) def page_reload(self, checked): w = self.tabs.currentWidget() if w is None: return w.reload() def page_search(self): w = self.tabs.currentWidget() if w is None: return w.findText(self.search_terms.text()) def delete(self): global _singleton _singleton = None ToolInstance.delete(self) def page_loaded(self, w, okay): if self.tabs.currentWidget() != w: return self.update_back_forward(w) def url_changed(self, w, url): if self.tabs.currentWidget() != w: return self.url.setText(_qurl2text(url)) self.update_back_forward(w) def title_changed(self, w, title): if self.tabs.currentWidget() == w: self.tool_window.title = title i = self.tabs.indexOf(w) self.tabs.setTabText(i, title) def link_hovered(self, url): from PyQt5.QtCore import QUrl try: self.status(_qurl2text(QUrl(url))) except Exception: self.status(url) def tab_changed(self, i): if i >= 0: tab_text = self.tabs.tabText(i) if tab_text != "New Tab": self.tool_window.title = tab_text self.url.setText(_qurl2text(self.tabs.currentWidget().url())) else: # no more tabs self.display(False) self.update_back_forward() def close_tab(self, i): w = self.tabs.widget(i) self.tabs.removeTab(i) w.deleteLater() self.update_back_forward() def close_current_tab(self): i = self.tabs.currentIndex() if i != -1: self.close_tab(i) def cycle_tab(self, incr): i = self.tabs.currentIndex() if i == -1: return count = self.tabs.count() i = (i + incr) % count self.tabs.setCurrentIndex(i) def tab_n(self, n): count = self.tabs.count() if count == 0: return if n == -1: self.tabs.setCurrentIndex(count - 1) elif n < count: self.tabs.setCurrentIndex(n) def update_back_forward(self, w=None): if w is None: w = self.tabs.currentWidget() history = w.history() self.back.setEnabled(history.canGoBack()) self.forward.setEnabled(history.canGoForward()) @classmethod def get_viewer(cls, session, target=None): global _singleton if _singleton is None: _singleton = HelpUI(session) return _singleton
def __init__(self, session, tool_name): self._default_color = (255, 153, 204, 128) # Transparent pink self._max_slider_value = 1000 # QSlider only handles integer values self._max_slider_radius = 100.0 # Float maximum radius value, scene units self._block_text_update = False # Avoid radius slider and text continuous updating each other. self._block_slider_update = False # Avoid radius slider and text continuous updating each other. b = session.main_view.drawing_bounds() vradius = 100 if b is None else b.radius() self._max_slider_radius = vradius center = b.center() if b else (0, 0, 0) self._sphere_model = SphereModel('eraser sphere', session, self._default_color, center, 0.2 * vradius) ToolInstance.__init__(self, session, tool_name) self.display_name = 'Map Eraser' from chimerax.ui import MainToolWindow tw = MainToolWindow(self) self.tool_window = tw parent = tw.ui_area from PyQt5.QtWidgets import QVBoxLayout, QHBoxLayout, QFrame, QCheckBox, QLabel, QPushButton, QLineEdit, QSlider from PyQt5.QtCore import Qt layout = QVBoxLayout(parent) layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) parent.setLayout(layout) sf = QFrame(parent) layout.addWidget(sf) slayout = QHBoxLayout(sf) slayout.setContentsMargins(0, 0, 0, 0) slayout.setSpacing(10) self._show_eraser = se = QCheckBox('Show map eraser sphere', sf) se.setCheckState(Qt.Checked) se.stateChanged.connect(self._show_eraser_cb) slayout.addWidget(se) from chimerax.ui.widgets import ColorButton self._sphere_color = sc = ColorButton(sf, max_size=(16, 16), has_alpha_channel=True) sc.color = self._default_color sc.color_changed.connect(self._change_color_cb) slayout.addWidget(sc) slayout.addStretch(1) # Extra space at end rf = QFrame(parent) layout.addWidget(rf) rlayout = QHBoxLayout(rf) rlayout.setContentsMargins(0, 0, 0, 0) rlayout.setSpacing(4) rl = QLabel('Radius', rf) rlayout.addWidget(rl) self._radius_entry = rv = QLineEdit('', rf) rv.setMaximumWidth(40) rv.returnPressed.connect(self._radius_changed_cb) rlayout.addWidget(rv) self._radius_slider = rs = QSlider(Qt.Horizontal, rf) smax = self._max_slider_value rs.setRange(0, smax) rs.valueChanged.connect(self._radius_slider_moved_cb) rlayout.addWidget(rs) rv.setText('%.4g' % self._sphere_model.radius) self._radius_changed_cb() ef = QFrame(parent) layout.addWidget(ef) elayout = QHBoxLayout(ef) elayout.setContentsMargins(0, 0, 0, 0) elayout.setSpacing(30) eb = QPushButton('Erase inside sphere', ef) eb.clicked.connect(self._erase_in_sphere) elayout.addWidget(eb) eo = QPushButton('Erase outside sphere', ef) eo.clicked.connect(self._erase_outside_sphere) elayout.addWidget(eo) rb = QPushButton('Reduce map bounds', ef) rb.clicked.connect(self._crop_map) elayout.addWidget(rb) elayout.addStretch(1) # Extra space at end layout.addStretch(1) # Extra space at end tw.manage(placement="side") # When displayed models change update radius slider range. from chimerax.core.models import MODEL_DISPLAY_CHANGED h = session.triggers.add_handler(MODEL_DISPLAY_CHANGED, self._model_display_change) self._model_display_change_handler = h
class AaronTools_Library(ToolInstance): help = "https://github.com/QChASM/SEQCROW/wiki/Browse-AaronTools-Libraries-Tool" SESSION_ENDURING = True SESSION_SAVE = True def __init__(self, session, name): super().__init__(session, name) from chimerax.ui import MainToolWindow self.tool_window = MainToolWindow(self) self.settings = _BrowseLibSettings(self.session, name) self.showLigKeyBool = True self.showSubGhostBool = True self.showRingWalkBool = True self._build_ui() def _build_ui(self): layout = QGridLayout() self.library_tabs = QTabWidget() #add a tab for ligands self.ligand_tab = QWidget() self.ligand_layout = QVBoxLayout(self.ligand_tab) self.lig_table = LigandTable() self.ligand_layout.addWidget(self.lig_table) showKeyAtomsCheck = QCheckBox('show key atoms') showKeyAtomsCheck.setToolTip( "ligand's coordinating atoms will be highlighted") showKeyAtomsCheck.toggle() showKeyAtomsCheck.stateChanged.connect(self.showKeyAtoms) self.ligand_layout.addWidget(showKeyAtomsCheck) self.lig_color = ColorButton('key atom color', has_alpha_channel=True) self.lig_color.setToolTip("highlight color for ligand's key atoms") self.lig_color.set_color(self.settings.key_atom_color) self.ligand_layout.addWidget(self.lig_color) openLigButton = QPushButton("open selected ligands") openLigButton.setToolTip( "ligands selected in the table will be loaded into ChimeraX") openLigButton.clicked.connect(self.open_ligands) self.ligand_layout.addWidget(openLigButton) self.openLigButton = openLigButton #add a tab for substituents self.substituent_tab = QWidget() self.substituent_layout = QVBoxLayout(self.substituent_tab) self.sub_table = SubstituentTable() self.substituent_layout.addWidget(self.sub_table) showGhostConnectionCheck = QCheckBox('show ghost connection') showGhostConnectionCheck.setToolTip( "ligand's coordinating atoms will be highlighted") showGhostConnectionCheck.toggle() showGhostConnectionCheck.stateChanged.connect(self.showGhostConnection) self.substituent_layout.addWidget(showGhostConnectionCheck) self.sub_color = ColorButton('ghost connection color', has_alpha_channel=True) self.sub_color.setToolTip("color of ghost connection") self.sub_color.set_color(self.settings.ghost_connection_color) self.substituent_layout.addWidget(self.sub_color) openSubButton = QPushButton("open selected substituents") openSubButton.setToolTip( "substituents selected in the table will be loaded into ChimeraX") openSubButton.clicked.connect(self.open_substituents) self.substituent_layout.addWidget(openSubButton) self.openSubButton = openSubButton #add a tab for rings self.ring_tab = QWidget() self.ring_layout = QVBoxLayout(self.ring_tab) self.ring_table = RingTable() self.ring_layout.addWidget(self.ring_table) showRingWalkCheck = QCheckBox('show ring walk') showRingWalkCheck.setToolTip( "arrows will show the way AaronTools traverses the ring") showRingWalkCheck.toggle() showRingWalkCheck.stateChanged.connect(self.showRingWalk) self.ring_layout.addWidget(showRingWalkCheck) self.ring_color = ColorButton('walk arrow color', has_alpha_channel=True) self.ring_color.setToolTip("color of walk arrows") self.ring_color.set_color(self.settings.ring_walk_color) self.ring_layout.addWidget(self.ring_color) openRingButton = QPushButton("open selected rings") openRingButton.setToolTip( "rings selected in the table will be loaded into ChimeraX") openRingButton.clicked.connect(self.open_rings) self.ring_layout.addWidget(openRingButton) self.openRingButton = openRingButton self.library_tabs.resize(300, 200) self.library_tabs.addTab(self.ligand_tab, "ligands") self.library_tabs.addTab(self.substituent_tab, "substituents") self.library_tabs.addTab(self.ring_tab, "rings") layout.addWidget(self.library_tabs) self.tool_window.ui_area.setLayout(layout) self.tool_window.manage(None) def showKeyAtoms(self, state): if state == QtCore.Qt.Checked: self.showLigKeyBool = True else: self.showLigKeyBool = False def open_ligands(self): for row in self.lig_table.table.selectionModel().selectedRows(): if self.lig_table.table.isRowHidden(row.row()): continue lig_name = row.data() ligand = Component(lig_name) chimera_ligand = ResidueCollection( ligand, name=lig_name).get_chimera(self.session) self.session.models.add([chimera_ligand]) apply_seqcrow_preset(chimera_ligand, fallback="Ball-Stick-Endcap") if self.showLigKeyBool: color = self.lig_color.get_color() color = [c / 255. for c in color] self.settings.key_atom_color = tuple(color) bild_obj = key_atom_highlight(ligand, color, self.session) self.session.models.add(bild_obj, parent=chimera_ligand) def showGhostConnection(self, state): if state == QtCore.Qt.Checked: self.showSubGhostBool = True else: self.showSubGhostBool = False def open_substituents(self): for row in self.sub_table.table.selectionModel().selectedRows(): if self.sub_table.table.isRowHidden(row.row()): continue sub_name = row.data() substituent = Substituent(sub_name, name=sub_name) chimera_substituent = ResidueCollection(substituent).get_chimera( self.session) self.session.models.add([chimera_substituent]) apply_seqcrow_preset(chimera_substituent, fallback="Ball-Stick-Endcap") if self.showSubGhostBool: color = self.sub_color.get_color() color = [c / 255. for c in color] self.settings.ghost_connection_color = tuple(color) bild_obj = ghost_connection_highlight(substituent, color, self.session) self.session.models.add(bild_obj, parent=chimera_substituent) def showRingWalk(self, state): if state == QtCore.Qt.Checked: self.showRingWalkBool = True else: self.showRingWalkBool = False def open_rings(self): for row in self.ring_table.table.selectionModel().selectedRows(): if self.ring_table.table.isRowHidden(row.row()): continue ring_name = row.data() ring = Ring(ring_name, name=ring_name) chimera_ring = ResidueCollection(ring.copy()).get_chimera( self.session) self.session.models.add([chimera_ring]) apply_seqcrow_preset(chimera_ring, fallback="Ball-Stick-Endcap") if self.showRingWalkBool: color = self.ring_color.get_color() color = [c / 255. for c in color] self.ring_walk_color = tuple(color) bild_obj = show_walk_highlight(ring, chimera_ring, color, self.session) self.session.models.add(bild_obj, parent=chimera_ring) def display_help(self): """Show the help for this tool in the help viewer.""" from chimerax.core.commands import run run(self.session, 'open %s' % self.help if self.help is not None else "")
class TutorialTool(ToolInstance): # Inheriting from ToolInstance makes us known to the ChimeraX tool mangager, # so we can be notified and take appropriate action when sessions are closed, # saved, or restored, and we will be listed among running tools and so on. # # If cleaning up is needed on finish, override the 'delete' method # but be sure to call 'delete' from the superclass at the end. SESSION_ENDURING = False # Does this instance persist when session closes SESSION_SAVE = True # We do save/restore in sessions help = "help:user/tools/tutorial.html" # Let ChimeraX know about our help page def __init__(self, session, tool_name): # 'session' - chimerax.core.session.Session instance # 'tool_name' - string # Initialize base class. super().__init__(session, tool_name) # Set name displayed on title bar (defaults to tool_name) # Must be after the superclass init, which would override it. self.display_name = "Tutorial — Qt-based" # Create the main window for our tool. The window object will have # a 'ui_area' where we place the widgets composing our interface. # The window isn't shown until we call its 'manage' method. # # Note that by default, tool windows are only hidden rather than # destroyed when the user clicks the window's close button. To change # this behavior, specify 'close_destroys=True' in the MainToolWindow # constructor. from chimerax.ui import MainToolWindow self.tool_window = MainToolWindow(self) # We will be adding an item to the tool's context menu, so override # the default MainToolWindow fill_context_menu method self.tool_window.fill_context_menu = self.fill_context_menu # Our user interface is simple enough that we could probably inline # the code right here, but for any kind of even moderately complex # interface, it is probably better to put the code in a method so # that this __init__ method remains readable. self._build_ui() def _build_ui(self): # Put our widgets in the tool window # We will use an editable single-line text input field (QLineEdit) # with a descriptive text label to the left of it (QLabel). To # arrange them horizontally side by side we use QHBoxLayout from PyQt5.QtWidgets import QLabel, QLineEdit, QHBoxLayout layout = QHBoxLayout() layout.addWidget(QLabel("Log this text:")) self.line_edit = QLineEdit() # Arrange for our 'return_pressed' method to be called when the # user presses the Return key self.line_edit.returnPressed.connect(self.return_pressed) layout.addWidget(self.line_edit) # Set the layout as the contents of our window self.tool_window.ui_area.setLayout(layout) # Show the window on the user-preferred side of the ChimeraX # main window self.tool_window.manage('side') def return_pressed(self): # The use has pressed the Return key; log the current text as HTML from chimerax.core.commands import run # ToolInstance has a 'session' attribute... run(self.session, "log html %s" % self.line_edit.text()) def fill_context_menu(self, menu, x, y): # Add any tool-specific items to the given context menu (a QMenu instance). # The menu will then be automatically filled out with generic tool-related actions # (e.g. Hide Tool, Help, Dockable Tool, etc.) # # The x,y args are the x() and y() values of QContextMenuEvent, in the rare case # where the items put in the menu depends on where in the tool interface the menu # was raised. from PyQt5.QtWidgets import QAction clear_action = QAction("Clear", menu) clear_action.triggered.connect(lambda *args: self.line_edit.clear()) menu.addAction(clear_action) def take_snapshot(self, session, flags): return { 'version': 1, 'current text': self.line_edit.text() } @classmethod def restore_snapshot(class_obj, session, data): # Instead of using a fixed string when calling the constructor below, we could # have saved the tool name during take_snapshot() (from self.tool_name, inherited # from ToolInstance) and used that saved tool name. There are pros and cons to # both approaches. inst = class_obj(session, "Tutorial (Qt)") inst.line_edit.setText(data['current text']) return inst
class HKLviewerTool(ToolInstance): # Inheriting from ToolInstance makes us known to the ChimeraX tool mangager, # so we can be notified and take appropriate action when sessions are closed, # saved, or restored, and we will be listed among running tools and so on. # # If cleaning up is needed on finish, override the 'delete' method # but be sure to call 'delete' from the superclass at the end. SESSION_ENDURING = False # Does this instance persist when session closes SESSION_SAVE = True # We do save/restore in sessions help = "help:user/tools/tutorial.html" # Let ChimeraX know about our help page def __init__(self, session, tool_name): # 'session' - chimerax.core.session.Session instance # 'tool_name' - string # Initialize base class. super().__init__(session, tool_name) # Set name displayed on title bar (defaults to tool_name) # Must be after the superclass init, which would override it. self.display_name = "HKLviewer" # Create the main window for our tool. The window object will have # a 'ui_area' where we place the widgets composing our interface. # The window isn't shown until we call its 'manage' method. # # Note that by default, tool windows are only hidden rather than # destroyed when the user clicks the window's close button. To change # this behavior, specify 'close_destroys=True' in the MainToolWindow # constructor. from chimerax.ui import MainToolWindow self.tool_window = MainToolWindow(self) # We will be adding an item to the tool's context menu, so override # the default MainToolWindow fill_context_menu method self.tool_window.fill_context_menu = self.fill_context_menu # Our user interface is simple enough that we could probably inline # the code right here, but for any kind of even moderately complex # interface, it is probably better to put the code in a method so # that this __init__ method remains readable. self._build_ui() self.clipper_crystdict = None session.HKLviewer = self def _build_ui(self): # Put our widgets in the tool window # We will use an editable single-line text input field (QLineEdit) # with a descriptive text label to the left of it (QLabel). To # arrange them horizontally side by side we use QHBoxLayout from Qt.QtWidgets import QHBoxLayout from . import HKLviewer hbox = QHBoxLayout() self.Guiobj = HKLviewer.run(isembedded=True, chimeraxsession=self.session) hbox.addWidget(self.Guiobj.window) self.tool_window.ui_area.setLayout(hbox) # Show the window on the user-preferred side of the ChimeraX # main window self.tool_window.manage('side') def isolde_clipper_data_to_dict(self): try: sh = self.session.isolde.selected_model.parent xmapset = sh.map_mgr.xmapsets[0] clipperlabel= list(xmapset.experimental_data.items())[0][0] labels = [lbl.strip() for lbl in clipperlabel.split(",")] self.clipper_crystdict = {} self.clipper_crystdict["spg_number"] = xmapset.spacegroup.spacegroup_number self.clipper_crystdict["unit_cell"] = (xmapset.unit_cell.cell.a, xmapset.unit_cell.cell.b, xmapset.unit_cell.cell.c, xmapset.unit_cell.cell.alpha*180/math.pi, xmapset.unit_cell.cell.beta*180/math.pi, xmapset.unit_cell.cell.gamma*180/math.pi) self.clipper_crystdict["HKL"] = xmapset.experimental_data[clipperlabel].data.data[0].tolist() self.clipper_crystdict[clipperlabel] = xmapset.experimental_data[clipperlabel].data.data[1].transpose().tolist() self.clipper_crystdict["FCALC,PHFCALC"] = xmapset.live_xmap_mgr.f_calc.data[1].transpose().tolist() self.clipper_crystdict["2FOFC,PH2FOFC"] = xmapset.live_xmap_mgr.base_2fofc.data[1].transpose().tolist() except Exception as e: pass def return_pressed(self): # The use has pressed the Return key; log the current text as HTML from chimerax.core.commands import run # ToolInstance has a 'session' attribute... run(self.session, "log html %s" % self.line_edit.text()) def fill_context_menu(self, menu, x, y): # Add any tool-specific items to the given context menu (a QMenu instance). # The menu will then be automatically filled out with generic tool-related actions # (e.g. Hide Tool, Help, Dockable Tool, etc.) # # The x,y args are the x() and y() values of QContextMenuEvent, in the rare case # where the items put in the menu depends on where in the tool interface the menu # was raised. from PyQt5.QtWidgets import QAction settings_action = QAction("HKLviewer settings", menu) settings_action.triggered.connect(lambda *args: self.Guiobj.SettingsDialog() ) menu.addAction(settings_action) def delete(self): from Qt.QtCore import QEvent self.session.triggers.remove_handler(self.Guiobj.chimeraxprocmsghandler) self.Guiobj.closeEvent(QEvent.Close) super(HKLviewerTool, self).delete() def take_snapshot(self, session, flags): return { 'version': 1, 'current text': self.line_edit.text() } @classmethod def restore_snapshot(class_obj, session, data): # Instead of using a fixed string when calling the constructor below, we could # have saved the tool name during take_snapshot() (from self.tool_name, inherited # from ToolInstance) and used that saved tool name. There are pros and cons to # both approaches. inst = class_obj(session, "cctbx.HKLviewer") inst.line_edit.setText(data['current text']) return inst
class RMFViewer(ToolInstance): SESSION_ENDURING = False # Does this instance persist when session closes SESSION_SAVE = True # We do save/restore in sessions help = "help:user/tools/rmf.html" def __init__(self, session, tool_name): super().__init__(session, tool_name) from chimerax.ui import MainToolWindow self.tool_window = MainToolWindow(self) self._build_ui() from chimerax.core.models import ADD_MODELS, REMOVE_MODELS session.triggers.add_handler(ADD_MODELS, self._fill_ui) session.triggers.add_handler(REMOVE_MODELS, self._fill_ui) self._fill_ui() def take_snapshot(self, session, flags): data = { 'version': 1, 'tool_name': self.tool_name, 'selmodel': self.model_list.currentIndex() } return data @classmethod def restore_snapshot(cls, session, data): t = cls(session, data['tool_name']) t.model_list.setCurrentIndex(data['selmodel']) return t def _fill_ui(self, *args): self.rmf_models = [ m for m in self.session.models.list() if hasattr(m, 'rmf_hierarchy') ] self.model_list.clear() self.model_list.addItems("%s; #%s" % (m.name, m.id_string) for m in self.rmf_models) self._fill_model_stack() def model_list_change(self, i): self.model_stack.setCurrentIndex(i) def _build_ui(self): layout = QtWidgets.QVBoxLayout() self.tool_window.ui_area.setLayout(layout) self.tool_window.manage('side') combo_and_label = QtWidgets.QHBoxLayout() label = QtWidgets.QLabel("RMF model") combo_and_label.addWidget(label) self.model_list = QtWidgets.QComboBox() self.model_list.currentIndexChanged.connect(self.model_list_change) combo_and_label.addWidget(self.model_list, stretch=4) layout.addLayout(combo_and_label) self.model_stack = QtWidgets.QStackedWidget() layout.addWidget(self.model_stack) def _fill_model_stack(self): self.model_stack.blockSignals(True) for i in range(self.model_stack.count()): self.model_stack.removeWidget(self.model_stack.widget(0)) for m in self.rmf_models: self.model_stack.addWidget(self._build_ui_rmf_model(m)) self.model_stack.blockSignals(False) def _build_ui_rmf_model(self, m): top = QtWidgets.QSplitter(Qt.Vertical) pane = QtWidgets.QWidget() layout = QtWidgets.QVBoxLayout() pane.setLayout(layout) layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) label = QtWidgets.QLabel("Features") layout.addWidget(label) tree = QtWidgets.QTreeView() tree.setAnimated(False) tree.setIndentation(20) tree.setSelectionMode(QtWidgets.QTreeView.ExtendedSelection) tree.setSortingEnabled(False) tree.setHeaderHidden(True) tree.setModel(_RMFFeaturesModel(m.rmf_features)) tree.selectionModel().selectionChanged.connect( lambda sel, desel, tree=tree: self._select_feature(tree)) layout.addWidget(tree) top.addWidget(pane) pane, hierarchy_tree = self._get_hierarchy_pane(m) top.addWidget(pane) pane = self._get_provenance_pane(m, hierarchy_tree) top.addWidget(pane) return top def _get_hierarchy_pane(self, m): pane = QtWidgets.QWidget() layout = QtWidgets.QVBoxLayout() pane.setLayout(layout) layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) label_and_res = QtWidgets.QHBoxLayout() label_and_res.setContentsMargins(0, 0, 0, 0) label_and_res.setSpacing(0) label = QtWidgets.QLabel("Hierarchy @") label_and_res.addWidget(label) tree = QtWidgets.QTreeView() for res in sorted(m._rmf_resolutions): cb = QtWidgets.QCheckBox("%.1f" % res) cb.setChecked(res in m._selected_rmf_resolutions) cb.clicked.connect( lambda *, cb=cb, tree=tree, resolution=res: self. _resolution_button_clicked(cb, tree, resolution)) label_and_res.addWidget(cb) layout.addLayout(label_and_res) tree.setAnimated(False) tree.setIndentation(20) tree.setSelectionMode(QtWidgets.QTreeView.ExtendedSelection) tree.setSortingEnabled(False) tree.setHeaderHidden(True) tree.setModel( _RMFHierarchyModel(m.rmf_hierarchy, m._selected_rmf_resolutions)) tree_and_buttons = QtWidgets.QHBoxLayout() tree_and_buttons.setContentsMargins(0, 0, 0, 0) tree_and_buttons.setSpacing(0) tree_and_buttons.addWidget(tree, stretch=1) buttons = QtWidgets.QVBoxLayout() buttons.setContentsMargins(0, 0, 0, 0) buttons.setSpacing(0) select_button = QtWidgets.QPushButton("Select") # In Qt one-argument callbacks get called with a bool argument; # throw this away select_button.clicked.connect( lambda *, tree=tree: self._select_button_clicked(tree)) buttons.addWidget(select_button) hide_button = QtWidgets.QPushButton("Hide") hide_button.clicked.connect( lambda *, tree=tree: self._hide_button_clicked(tree)) buttons.addWidget(hide_button) show_button = QtWidgets.QPushButton("Show") show_button.clicked.connect( lambda *, tree=tree: self._show_button_clicked(tree)) buttons.addWidget(show_button) show_only_button = QtWidgets.QPushButton("Only") show_only_button.clicked.connect( lambda *, tree=tree: self._show_only_button_clicked(tree)) buttons.addWidget(show_only_button) view_button = QtWidgets.QPushButton("View") view_button.clicked.connect( lambda *, tree=tree: self._view_button_clicked(tree)) buttons.addWidget(view_button) tree_and_buttons.addLayout(buttons) layout.addLayout(tree_and_buttons, stretch=1) return pane, tree def _get_provenance_pane(self, m, hierarchy_tree): pane = QtWidgets.QWidget() layout = QtWidgets.QVBoxLayout() pane.setLayout(layout) layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) label = QtWidgets.QLabel("Provenance") layout.addWidget(label) tree = QtWidgets.QTreeView() tree.setAnimated(False) tree.setIndentation(20) tree.setSelectionMode(QtWidgets.QTreeView.ExtendedSelection) tree.setSortingEnabled(False) tree.setHeaderHidden(True) tree.setModel(_RMFProvenanceModel(m.rmf_provenance)) tree.selectionModel().selectionChanged.connect( lambda sel, desel, tree=tree, hierarchy_tree=hierarchy_tree: self. _select_provenance(tree, hierarchy_tree)) tree_and_buttons = QtWidgets.QHBoxLayout() tree_and_buttons.setContentsMargins(0, 0, 0, 0) tree_and_buttons.setSpacing(0) tree_and_buttons.addWidget(tree, stretch=1) buttons = QtWidgets.QVBoxLayout() buttons.setContentsMargins(0, 0, 0, 0) buttons.setSpacing(0) load_button = QtWidgets.QPushButton("Load") load_button.clicked.connect( lambda *, tree=tree, m=m: self._load_button_clicked(tree, m)) buttons.addWidget(load_button) tree_and_buttons.addLayout(buttons) layout.addLayout(tree_and_buttons, stretch=1) return pane def _get_selected_chimera_objects(self, tree): def _get_node_objects(node, objs): o = node.chimera_obj if o and not o.deleted: objs.append(o) for child in node._filtered_children: _get_node_objects(child, objs) objs = [] inds = tree.selectedIndexes() for ind in inds: _get_node_objects(ind.internalPointer(), objs) # If empty selection, use the root instead if not inds: _get_node_objects(tree.model().rmf_hierarchy, objs) objects = Objects() objects.add_atoms(Atoms(x for x in objs if isinstance(x, Atom))) objects.add_bonds(Bonds(x for x in objs if isinstance(x, Bond))) return objects def _get_selected_features(self, tree): def get_child_chimera_obj(feat): for child in feat.children: o = child.chimera_obj if o: yield o for obj in get_child_chimera_obj(child): yield obj def get_selection(): for f in tree.selectedIndexes(): feat = f.internalPointer() obj = feat.chimera_obj # Prefer to select pseudobonds (even from children) if (obj is not None and (not isinstance(obj, Atoms) or not feat.children)): yield obj else: for obj in get_child_chimera_obj(feat): yield obj s = list(get_selection()) objs = Objects() objs.add_pseudobonds( Pseudobonds(x for x in s if isinstance(x, Pseudobond))) for x in s: if isinstance(x, Atoms): objs.add_atoms(x) return objs def _select_button_clicked(self, tree): from chimerax.std_commands.select import select select(self.session, self._get_selected_chimera_objects(tree)) def _show_button_clicked(self, tree): from chimerax.std_commands.show import show show(self.session, self._get_selected_chimera_objects(tree)) def _hide_button_clicked(self, tree): from chimerax.std_commands.hide import hide hide(self.session, self._get_selected_chimera_objects(tree)) def _view_button_clicked(self, tree): from chimerax.std_commands.view import view view(self.session, self._get_selected_chimera_objects(tree)) def _show_only_button_clicked(self, tree): def show_only(node, show_roots, under_root=False, show=True): if not under_root and show and node in show_roots: under_root = True o = node.chimera_obj if o: o.display = under_root and show if under_root: to_show = frozenset(node._filtered_children) for child in node.children: show_only(child, show_roots, under_root, child in to_show) else: for child in node.children: show_only(child, show_roots, under_root) show_roots = frozenset(ind.internalPointer() for ind in tree.selectedIndexes()) top = tree.model().rmf_hierarchy if not show_roots: show_roots = frozenset([top]) show_only(top, show_roots) def _select_feature(self, tree): from chimerax.std_commands.select import select select(self.session, self._get_selected_features(tree)) def _select_provenance(self, tree, hierarchy_tree): mode = QItemSelectionModel.ClearAndSelect hierarchy_model = hierarchy_tree.model() hierarchy_selmodel = hierarchy_tree.selectionModel() for f in tree.selectedIndexes(): obj = f.internalPointer() if obj.hierarchy_node: ind = hierarchy_model.index_for_node(obj.hierarchy_node) if ind.isValid(): hierarchy_selmodel.setCurrentIndex(ind, mode) mode = QItemSelectionModel.Select def _load_button_clicked(self, tree, m): m._update_provenance_map() objs = [f.internalPointer() for f in tree.selectedIndexes()] # If empty selection, load everything for obj in objs or tree.model().rmf_provenance: obj.load(self.session, m) def _resolution_button_clicked(self, checkbox, tree, resolution): model = tree.model() selmodel = tree.selectionModel() selected = [ ind.internalPointer() for ind in selmodel.selectedIndexes() ] model.set_resolution_filter(resolution, checkbox.isChecked()) mode = QItemSelectionModel.ClearAndSelect for s in selected: ind = model.index_for_node(s) if ind.isValid(): selmodel.setCurrentIndex(ind, mode) mode = QItemSelectionModel.Select