def clone_action(ac, parent): if ac.isSeparator(): ans = QAction(parent) ans.setSeparator(True) return ans sc = ac.shortcut() sc = '' if sc.isEmpty() else sc.toString(sc.NativeText) ans = QAction(ac.icon(), ac.text() + '\t' + sc, parent) ans.triggered.connect(ac.trigger) ans.setEnabled(ac.isEnabled()) ans.setStatusTip(ac.statusTip()) ans.setVisible(ac.isVisible()) return ans
def clone_action(ac, parent): if ac.isSeparator(): ans = QAction(parent) ans.setSeparator(True) return ans sc = ac.shortcut() sc = '' if sc.isEmpty() else sc.toString(sc.NativeText) ans = QAction(ac.icon(), ac.text() + '\t' + sc, parent) ans.triggered.connect(ac.trigger) ans.setEnabled(ac.isEnabled()) ans.setStatusTip(ac.statusTip()) ans.setVisible(ac.isVisible()) return ans
def clone_action(ac, parent): if ac.isSeparator(): ans = QAction(parent) ans.setSeparator(True) return ans sc = ac.shortcut() sc = '' if sc.isEmpty() else sc.toString( QKeySequence.SequenceFormat.NativeText) text = ac.text() if '\t' not in text: text += '\t' + sc ans = QAction(ac.icon(), text, parent) ans.triggered.connect(ac.trigger) ans.setEnabled(ac.isEnabled()) ans.setStatusTip(ac.statusTip()) ans.setVisible(ac.isVisible()) return ans
def contextMenuEvent(self, event): ''' @param: event QContextMenuEvent ''' if not self._menu: self._menu = QMenu(self) for idx in range(self.count()): act = QAction(self.model().headerData(idx, Qt.Horizontal), self._menu) act.setCheckable(True) act.setData(idx) act.triggered.connect(self._toggleSectionVisibility) self._menu.addAction(act) for idx, act in enumerate(self._menu.actions()): act.setEnabled(idx > 0) act.setChecked(not self.isSectionHidden(idx)) self._menu.popup(event.globalPos())
class SortColumnListWidget(ColumnListWidget): def __init__(self, parent, gui): ColumnListWidget.__init__(self, parent, gui) self.create_context_menu() self.itemChanged.connect(self.set_sort_icon) self.itemSelectionChanged.connect(self.item_selection_changed) def create_context_menu(self): self.setContextMenuPolicy(Qt.ActionsContextMenu) self.sort_ascending_action = QAction('Sort ascending', self) self.sort_ascending_action.setIcon(get_icon('images/sort_asc.png')) self.sort_ascending_action.triggered.connect( partial(self.change_sort, 0)) self.addAction(self.sort_ascending_action) self.sort_descending_action = QAction('Sort descending', self) self.sort_descending_action.setIcon(get_icon('images/sort_desc.png')) self.sort_descending_action.triggered.connect( partial(self.change_sort, 1)) self.addAction(self.sort_descending_action) def populate(self, columns, all_columns): self.blockSignals(True) all_columns = [colname for colname, _width in all_columns] self.clear() for col, asc in columns: if col in all_columns: all_columns.remove(col) self.populate_column(col, asc, is_checked=True) if len(all_columns) > 0: for col in all_columns: self.populate_column(col, 0, is_checked=False) self.blockSignals(False) def populate_column(self, col, asc, is_checked): item = QListWidgetItem(self.gui.library_view.model().headers[col], self) item.setData(Qt.UserRole, col + '|' + str(asc)) flags = Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsUserCheckable item.setFlags(flags) item.setCheckState(Qt.Checked if is_checked else Qt.Unchecked) self.set_sort_icon(item) def set_sort_icon(self, item): previous = self.blockSignals(True) if item.checkState() == Qt.Checked: data = convert_qvariant(item.data(Qt.UserRole)).strip() asc = int(data.rpartition('|')[2]) if asc == 0: item.setIcon(get_icon('images/sort_asc.png')) else: item.setIcon(get_icon('images/sort_desc.png')) else: item.setIcon(QIcon()) self.item_selection_changed( ) ## otherwise asc/desc can be disabled if selected, then checked. self.blockSignals(previous) def item_selection_changed(self): self.sort_ascending_action.setEnabled(False) self.sort_descending_action.setEnabled(False) item = self.currentItem() if item and item.checkState() == Qt.Checked: self.sort_ascending_action.setEnabled(True) self.sort_descending_action.setEnabled(True) def change_sort(self, asc): item = self.currentItem() if item: self.blockSignals(True) data = convert_qvariant(item.data(Qt.UserRole)).strip().split('|') col = data[0] item.setData(Qt.UserRole, col + '|' + str(asc)) self.set_sort_icon(item) self.blockSignals(False) def get_data(self): cols = [] for idx in range(self.count()): item = self.item(idx) data = convert_qvariant(item.data(Qt.UserRole)).strip().split('|') if item.checkState() == Qt.Checked: cols.append((data[0], int(data[1]))) return cols
class PluginUpdaterDialog(SizePersistedDialog): initial_extra_size = QSize(350, 100) forum_label_text = _('Plugin homepage') def __init__(self, gui, initial_filter=FILTER_UPDATE_AVAILABLE): SizePersistedDialog.__init__(self, gui, 'Plugin Updater plugin:plugin updater dialog') self.gui = gui self.forum_link = None self.zip_url = None self.model = None self.do_restart = False self._initialize_controls() self._create_context_menu() try: display_plugins = read_available_plugins(raise_error=True) except Exception: display_plugins = [] import traceback error_dialog(self.gui, _('Update Check Failed'), _('Unable to reach the plugin index page.'), det_msg=traceback.format_exc(), show=True) if display_plugins: self.model = DisplayPluginModel(display_plugins) self.proxy_model = DisplayPluginSortFilterModel(self) self.proxy_model.setSourceModel(self.model) self.plugin_view.setModel(self.proxy_model) self.plugin_view.resizeColumnsToContents() self.plugin_view.selectionModel().currentRowChanged.connect(self._plugin_current_changed) self.plugin_view.doubleClicked.connect(self.install_button.click) self.filter_combo.setCurrentIndex(initial_filter) self._select_and_focus_view() else: self.filter_combo.setEnabled(False) # Cause our dialog size to be restored from prefs or created on first usage self.resize_dialog() def _initialize_controls(self): self.setWindowTitle(_('User plugins')) self.setWindowIcon(QIcon(I('plugins/plugin_updater.png'))) layout = QVBoxLayout(self) self.setLayout(layout) title_layout = ImageTitleLayout(self, 'plugins/plugin_updater.png', _('User plugins')) layout.addLayout(title_layout) header_layout = QHBoxLayout() layout.addLayout(header_layout) self.filter_combo = PluginFilterComboBox(self) self.filter_combo.setMinimumContentsLength(20) self.filter_combo.currentIndexChanged[int].connect(self._filter_combo_changed) la = QLabel(_('Filter list of &plugins')+':', self) la.setBuddy(self.filter_combo) header_layout.addWidget(la) header_layout.addWidget(self.filter_combo) header_layout.addStretch(10) # filter plugins by name la = QLabel(_('Filter by &name')+':', self) header_layout.addWidget(la) self.filter_by_name_lineedit = QLineEdit(self) la.setBuddy(self.filter_by_name_lineedit) self.filter_by_name_lineedit.setText("") self.filter_by_name_lineedit.textChanged.connect(self._filter_name_lineedit_changed) header_layout.addWidget(self.filter_by_name_lineedit) self.plugin_view = QTableView(self) self.plugin_view.horizontalHeader().setStretchLastSection(True) self.plugin_view.setSelectionBehavior(QAbstractItemView.SelectRows) self.plugin_view.setSelectionMode(QAbstractItemView.SingleSelection) self.plugin_view.setAlternatingRowColors(True) self.plugin_view.setSortingEnabled(True) self.plugin_view.setIconSize(QSize(28, 28)) layout.addWidget(self.plugin_view) details_layout = QHBoxLayout() layout.addLayout(details_layout) forum_label = self.forum_label = QLabel('') forum_label.setTextInteractionFlags(Qt.LinksAccessibleByMouse | Qt.LinksAccessibleByKeyboard) forum_label.linkActivated.connect(self._forum_label_activated) details_layout.addWidget(QLabel(_('Description')+':', self), 0, Qt.AlignLeft) details_layout.addWidget(forum_label, 1, Qt.AlignRight) self.description = QLabel(self) self.description.setFrameStyle(QFrame.Panel | QFrame.Sunken) self.description.setAlignment(Qt.AlignTop | Qt.AlignLeft) self.description.setMinimumHeight(40) self.description.setWordWrap(True) layout.addWidget(self.description) self.button_box = QDialogButtonBox(QDialogButtonBox.Close) self.button_box.rejected.connect(self.reject) self.finished.connect(self._finished) self.install_button = self.button_box.addButton(_('&Install'), QDialogButtonBox.AcceptRole) self.install_button.setToolTip(_('Install the selected plugin')) self.install_button.clicked.connect(self._install_clicked) self.install_button.setEnabled(False) self.configure_button = self.button_box.addButton(' '+_('&Customize plugin ')+' ', QDialogButtonBox.ResetRole) self.configure_button.setToolTip(_('Customize the options for this plugin')) self.configure_button.clicked.connect(self._configure_clicked) self.configure_button.setEnabled(False) layout.addWidget(self.button_box) def update_forum_label(self): txt = '' if self.forum_link: txt = '<a href="%s">%s</a>' % (self.forum_link, self.forum_label_text) self.forum_label.setText(txt) def _create_context_menu(self): self.plugin_view.setContextMenuPolicy(Qt.ActionsContextMenu) self.install_action = QAction(QIcon(I('plugins/plugin_upgrade_ok.png')), _('&Install'), self) self.install_action.setToolTip(_('Install the selected plugin')) self.install_action.triggered.connect(self._install_clicked) self.install_action.setEnabled(False) self.plugin_view.addAction(self.install_action) self.history_action = QAction(QIcon(I('chapters.png')), _('Version &history'), self) self.history_action.setToolTip(_('Show history of changes to this plugin')) self.history_action.triggered.connect(self._history_clicked) self.history_action.setEnabled(False) self.plugin_view.addAction(self.history_action) self.forum_action = QAction(QIcon(I('plugins/mobileread.png')), _('Plugin &forum thread'), self) self.forum_action.triggered.connect(self._forum_label_activated) self.forum_action.setEnabled(False) self.plugin_view.addAction(self.forum_action) sep1 = QAction(self) sep1.setSeparator(True) self.plugin_view.addAction(sep1) self.toggle_enabled_action = QAction(_('Enable/&disable plugin'), self) self.toggle_enabled_action.setToolTip(_('Enable or disable this plugin')) self.toggle_enabled_action.triggered.connect(self._toggle_enabled_clicked) self.toggle_enabled_action.setEnabled(False) self.plugin_view.addAction(self.toggle_enabled_action) self.uninstall_action = QAction(_('&Remove plugin'), self) self.uninstall_action.setToolTip(_('Uninstall the selected plugin')) self.uninstall_action.triggered.connect(self._uninstall_clicked) self.uninstall_action.setEnabled(False) self.plugin_view.addAction(self.uninstall_action) sep2 = QAction(self) sep2.setSeparator(True) self.plugin_view.addAction(sep2) self.donate_enabled_action = QAction(QIcon(I('donate.png')), _('Donate to developer'), self) self.donate_enabled_action.setToolTip(_('Donate to the developer of this plugin')) self.donate_enabled_action.triggered.connect(self._donate_clicked) self.donate_enabled_action.setEnabled(False) self.plugin_view.addAction(self.donate_enabled_action) sep3 = QAction(self) sep3.setSeparator(True) self.plugin_view.addAction(sep3) self.configure_action = QAction(QIcon(I('config.png')), _('&Customize plugin'), self) self.configure_action.setToolTip(_('Customize the options for this plugin')) self.configure_action.triggered.connect(self._configure_clicked) self.configure_action.setEnabled(False) self.plugin_view.addAction(self.configure_action) def _finished(self, *args): if self.model: update_plugins = list(filter(filter_upgradeable_plugins, self.model.display_plugins)) self.gui.recalc_update_label(len(update_plugins)) def _plugin_current_changed(self, current, previous): if current.isValid(): actual_idx = self.proxy_model.mapToSource(current) display_plugin = self.model.display_plugins[actual_idx.row()] self.description.setText(display_plugin.description) self.forum_link = display_plugin.forum_link self.zip_url = display_plugin.zip_url self.forum_action.setEnabled(bool(self.forum_link)) self.install_button.setEnabled(display_plugin.is_valid_to_install()) self.install_action.setEnabled(self.install_button.isEnabled()) self.uninstall_action.setEnabled(display_plugin.is_installed()) self.history_action.setEnabled(display_plugin.has_changelog) self.configure_button.setEnabled(display_plugin.is_installed()) self.configure_action.setEnabled(self.configure_button.isEnabled()) self.toggle_enabled_action.setEnabled(display_plugin.is_installed()) self.donate_enabled_action.setEnabled(bool(display_plugin.donation_link)) else: self.description.setText('') self.forum_link = None self.zip_url = None self.forum_action.setEnabled(False) self.install_button.setEnabled(False) self.install_action.setEnabled(False) self.uninstall_action.setEnabled(False) self.history_action.setEnabled(False) self.configure_button.setEnabled(False) self.configure_action.setEnabled(False) self.toggle_enabled_action.setEnabled(False) self.donate_enabled_action.setEnabled(False) self.update_forum_label() def _donate_clicked(self): plugin = self._selected_display_plugin() if plugin and plugin.donation_link: open_url(QUrl(plugin.donation_link)) def _select_and_focus_view(self, change_selection=True): if change_selection and self.plugin_view.model().rowCount() > 0: self.plugin_view.selectRow(0) else: idx = self.plugin_view.selectionModel().currentIndex() self._plugin_current_changed(idx, 0) self.plugin_view.setFocus() def _filter_combo_changed(self, idx): self.filter_by_name_lineedit.setText("") # clear the name filter text when a different group was selected self.proxy_model.set_filter_criteria(idx) if idx == FILTER_NOT_INSTALLED: self.plugin_view.sortByColumn(5, Qt.DescendingOrder) else: self.plugin_view.sortByColumn(0, Qt.AscendingOrder) self._select_and_focus_view() def _filter_name_lineedit_changed(self, text): self.proxy_model.set_filter_text(text) # set the filter text for filterAcceptsRow def _forum_label_activated(self): if self.forum_link: open_url(QUrl(self.forum_link)) def _selected_display_plugin(self): idx = self.plugin_view.selectionModel().currentIndex() actual_idx = self.proxy_model.mapToSource(idx) return self.model.display_plugins[actual_idx.row()] def _uninstall_plugin(self, name_to_remove): if DEBUG: prints('Removing plugin: ', name_to_remove) remove_plugin(name_to_remove) # Make sure that any other plugins that required this plugin # to be uninstalled first have the requirement removed for display_plugin in self.model.display_plugins: # Make sure we update the status and display of the # plugin we just uninstalled if name_to_remove in display_plugin.uninstall_plugins: if DEBUG: prints('Removing uninstall dependency for: ', display_plugin.name) display_plugin.uninstall_plugins.remove(name_to_remove) if display_plugin.qname == name_to_remove: if DEBUG: prints('Resetting plugin to uninstalled status: ', display_plugin.name) display_plugin.installed_version = None display_plugin.plugin = None display_plugin.uninstall_plugins = [] if self.proxy_model.filter_criteria not in [FILTER_INSTALLED, FILTER_UPDATE_AVAILABLE]: self.model.refresh_plugin(display_plugin) def _uninstall_clicked(self): display_plugin = self._selected_display_plugin() if not question_dialog(self, _('Are you sure?'), '<p>'+ _('Are you sure you want to uninstall the <b>%s</b> plugin?')%display_plugin.name, show_copy_button=False): return self._uninstall_plugin(display_plugin.qname) if self.proxy_model.filter_criteria in [FILTER_INSTALLED, FILTER_UPDATE_AVAILABLE]: self.model.beginResetModel(), self.model.endResetModel() self._select_and_focus_view() else: self._select_and_focus_view(change_selection=False) def _install_clicked(self): display_plugin = self._selected_display_plugin() if not question_dialog(self, _('Install %s')%display_plugin.name, '<p>' + _('Installing plugins is a <b>security risk</b>. ' 'Plugins can contain a virus/malware. ' 'Only install it if you got it from a trusted source.' ' Are you sure you want to proceed?'), show_copy_button=False): return if display_plugin.uninstall_plugins: uninstall_names = list(display_plugin.uninstall_plugins) if DEBUG: prints('Uninstalling plugin: ', ', '.join(uninstall_names)) for name_to_remove in uninstall_names: self._uninstall_plugin(name_to_remove) plugin_zip_url = display_plugin.zip_url if DEBUG: prints('Downloading plugin ZIP attachment: ', plugin_zip_url) self.gui.status_bar.showMessage(_('Downloading plugin ZIP attachment: %s') % plugin_zip_url) zip_path = self._download_zip(plugin_zip_url) if DEBUG: prints('Installing plugin: ', zip_path) self.gui.status_bar.showMessage(_('Installing plugin: %s') % zip_path) do_restart = False try: from calibre.customize.ui import config installed_plugins = frozenset(config['plugins']) try: plugin = add_plugin(zip_path) except NameConflict as e: return error_dialog(self.gui, _('Already exists'), unicode_type(e), show=True) # Check for any toolbars to add to. widget = ConfigWidget(self.gui) widget.gui = self.gui widget.check_for_add_to_toolbars(plugin, previously_installed=plugin.name in installed_plugins) self.gui.status_bar.showMessage(_('Plugin installed: %s') % display_plugin.name) d = info_dialog(self.gui, _('Success'), _('Plugin <b>{0}</b> successfully installed under <b>' ' {1} plugins</b>. You may have to restart calibre ' 'for the plugin to take effect.').format(plugin.name, plugin.type), show_copy_button=False) b = d.bb.addButton(_('&Restart calibre now'), d.bb.AcceptRole) b.setIcon(QIcon(I('lt.png'))) d.do_restart = False def rf(): d.do_restart = True b.clicked.connect(rf) d.set_details('') d.exec_() b.clicked.disconnect() do_restart = d.do_restart display_plugin.plugin = plugin # We cannot read the 'actual' version information as the plugin will not be loaded yet display_plugin.installed_version = display_plugin.available_version except: if DEBUG: prints('ERROR occurred while installing plugin: %s'%display_plugin.name) traceback.print_exc() error_dialog(self.gui, _('Install plugin failed'), _('A problem occurred while installing this plugin.' ' This plugin will now be uninstalled.' ' Please post the error message in details below into' ' the forum thread for this plugin and restart calibre.'), det_msg=traceback.format_exc(), show=True) if DEBUG: prints('Due to error now uninstalling plugin: %s'%display_plugin.name) remove_plugin(display_plugin.name) display_plugin.plugin = None display_plugin.uninstall_plugins = [] if self.proxy_model.filter_criteria in [FILTER_NOT_INSTALLED, FILTER_UPDATE_AVAILABLE]: self.model.beginResetModel(), self.model.endResetModel() self._select_and_focus_view() else: self.model.refresh_plugin(display_plugin) self._select_and_focus_view(change_selection=False) if do_restart: self.do_restart = True self.accept() def _history_clicked(self): display_plugin = self._selected_display_plugin() text = self._read_version_history_html(display_plugin.forum_link) if text: dlg = VersionHistoryDialog(self, display_plugin.name, text) dlg.exec_() else: return error_dialog(self, _('Version history missing'), _('Unable to find the version history for %s')%display_plugin.name, show=True) def _configure_clicked(self): display_plugin = self._selected_display_plugin() plugin = display_plugin.plugin if not plugin.is_customizable(): return info_dialog(self, _('Plugin not customizable'), _('Plugin: %s does not need customization')%plugin.name, show=True) from calibre.customize import InterfaceActionBase if isinstance(plugin, InterfaceActionBase) and not getattr(plugin, 'actual_iaction_plugin_loaded', False): return error_dialog(self, _('Must restart'), _('You must restart calibre before you can' ' configure the <b>%s</b> plugin')%plugin.name, show=True) plugin.do_user_config(self.parent()) def _toggle_enabled_clicked(self): display_plugin = self._selected_display_plugin() plugin = display_plugin.plugin if not plugin.can_be_disabled: return error_dialog(self,_('Plugin cannot be disabled'), _('The plugin: %s cannot be disabled')%plugin.name, show=True) if is_disabled(plugin): enable_plugin(plugin) else: disable_plugin(plugin) self.model.refresh_plugin(display_plugin) def _read_version_history_html(self, forum_link): br = browser() br.set_handle_gzip(True) try: raw = br.open_novisit(forum_link).read() if not raw: return None except: traceback.print_exc() return None raw = raw.decode('utf-8', errors='replace') root = html.fromstring(raw) spoiler_nodes = root.xpath('//div[@class="smallfont" and strong="Spoiler"]') for spoiler_node in spoiler_nodes: try: if spoiler_node.getprevious() is None: # This is a spoiler node that has been indented using [INDENT] # Need to go up to parent div, then previous node to get header heading_node = spoiler_node.getparent().getprevious() else: # This is a spoiler node after a BR tag from the heading heading_node = spoiler_node.getprevious().getprevious() if heading_node is None: continue if heading_node.text_content().lower().find('version history') != -1: div_node = spoiler_node.xpath('div')[0] text = html.tostring(div_node, method='html', encoding='unicode') return re.sub(r'<div\s.*?>', '<div>', text) except: if DEBUG: prints('======= MobileRead Parse Error =======') traceback.print_exc() prints(html.tostring(spoiler_node)) return None def _download_zip(self, plugin_zip_url): from calibre.ptempfile import PersistentTemporaryFile raw = get_https_resource_securely(plugin_zip_url, headers={'User-Agent':'%s %s' % (__appname__, __version__)}) with PersistentTemporaryFile('.zip') as pt: pt.write(raw) return pt.name
class EditorWidget(QWebView, LineEditECM): # {{{ data_changed = pyqtSignal() def __init__(self, parent=None): QWebView.__init__(self, parent) self.base_url = None self._parent = weakref.ref(parent) self.readonly = False self.comments_pat = re.compile(r'<!--.*?-->', re.DOTALL) extra_shortcuts = { 'ToggleBold': 'Bold', 'ToggleItalic': 'Italic', 'ToggleUnderline': 'Underline', } for wac, name, icon, text, checkable in [ ('ToggleBold', 'bold', 'format-text-bold', _('Bold'), True), ('ToggleItalic', 'italic', 'format-text-italic', _('Italic'), True), ('ToggleUnderline', 'underline', 'format-text-underline', _('Underline'), True), ('ToggleStrikethrough', 'strikethrough', 'format-text-strikethrough', _('Strikethrough'), True), ('ToggleSuperscript', 'superscript', 'format-text-superscript', _('Superscript'), True), ('ToggleSubscript', 'subscript', 'format-text-subscript', _('Subscript'), True), ('InsertOrderedList', 'ordered_list', 'format-list-ordered', _('Ordered list'), True), ('InsertUnorderedList', 'unordered_list', 'format-list-unordered', _('Unordered list'), True), ('AlignLeft', 'align_left', 'format-justify-left', _('Align left'), False), ('AlignCenter', 'align_center', 'format-justify-center', _('Align center'), False), ('AlignRight', 'align_right', 'format-justify-right', _('Align right'), False), ('AlignJustified', 'align_justified', 'format-justify-fill', _('Align justified'), False), ('Undo', 'undo', 'edit-undo', _('Undo'), False), ('Redo', 'redo', 'edit-redo', _('Redo'), False), ('RemoveFormat', 'remove_format', 'edit-clear', _('Remove formatting'), False), ('Copy', 'copy', 'edit-copy', _('Copy'), False), ('Paste', 'paste', 'edit-paste', _('Paste'), False), ('Cut', 'cut', 'edit-cut', _('Cut'), False), ('Indent', 'indent', 'format-indent-more', _('Increase indentation'), False), ('Outdent', 'outdent', 'format-indent-less', _('Decrease indentation'), False), ('SelectAll', 'select_all', 'edit-select-all', _('Select all'), False), ]: ac = PageAction(wac, icon, text, checkable, self) setattr(self, 'action_' + name, ac) ss = extra_shortcuts.get(wac, None) if ss: ac.setShortcut(QKeySequence(getattr(QKeySequence, ss))) if wac == 'RemoveFormat': ac.triggered.connect(self.remove_format_cleanup, type=Qt.QueuedConnection) self.action_color = QAction(QIcon(I('format-text-color.png')), _('Foreground color'), self) self.action_color.triggered.connect(self.foreground_color) self.action_background = QAction(QIcon(I('format-fill-color.png')), _('Background color'), self) self.action_background.triggered.connect(self.background_color) self.action_block_style = QAction(QIcon(I('format-text-heading.png')), _('Style text block'), self) self.action_block_style.setToolTip(_('Style the selected text block')) self.block_style_menu = QMenu(self) self.action_block_style.setMenu(self.block_style_menu) self.block_style_actions = [] for text, name in [ (_('Normal'), 'p'), (_('Heading') + ' 1', 'h1'), (_('Heading') + ' 2', 'h2'), (_('Heading') + ' 3', 'h3'), (_('Heading') + ' 4', 'h4'), (_('Heading') + ' 5', 'h5'), (_('Heading') + ' 6', 'h6'), (_('Pre-formatted'), 'pre'), (_('Blockquote'), 'blockquote'), (_('Address'), 'address'), ]: ac = BlockStyleAction(text, name, self) self.block_style_menu.addAction(ac) self.block_style_actions.append(ac) self.action_insert_link = QAction(QIcon(I('insert-link.png')), _('Insert link or image'), self) self.action_insert_hr = QAction(QIcon(I('format-text-hr.png')), _('Insert separator'), self) self.action_insert_link.triggered.connect(self.insert_link) self.action_insert_hr.triggered.connect(self.insert_hr) self.pageAction(QWebPage.ToggleBold).changed.connect( self.update_link_action) self.action_insert_link.setEnabled(False) self.action_insert_hr.setEnabled(False) self.action_clear = QAction(QIcon(I('trash.png')), _('Clear'), self) self.action_clear.triggered.connect(self.clear_text) self.page().setLinkDelegationPolicy(QWebPage.DelegateAllLinks) self.page().linkClicked.connect(self.link_clicked) secure_web_page(self.page().settings()) self.setHtml('') self.set_readonly(False) self.page().contentsChanged.connect(self.data_changed) def update_link_action(self): wac = self.pageAction(QWebPage.ToggleBold).isEnabled() self.action_insert_link.setEnabled(wac) self.action_insert_hr.setEnabled(wac) def set_readonly(self, what): self.readonly = what self.page().setContentEditable(not self.readonly) def clear_text(self, *args): us = self.page().undoStack() us.beginMacro('clear all text') self.action_select_all.trigger() self.action_remove_format.trigger() self.exec_command('delete') us.endMacro() self.set_font_style() self.setFocus(Qt.OtherFocusReason) def link_clicked(self, url): open_url(url) def foreground_color(self): col = QColorDialog.getColor(Qt.black, self, _('Choose foreground color'), QColorDialog.ShowAlphaChannel) if col.isValid(): self.exec_command('foreColor', unicode_type(col.name())) def background_color(self): col = QColorDialog.getColor(Qt.white, self, _('Choose background color'), QColorDialog.ShowAlphaChannel) if col.isValid(): self.exec_command('hiliteColor', unicode_type(col.name())) def insert_hr(self, *args): self.exec_command('insertHTML', '<hr>') def insert_link(self, *args): link, name, is_image = self.ask_link() if not link: return url = self.parse_link(link) if url.isValid(): url = unicode_type(url.toString(NO_URL_FORMATTING)) self.setFocus(Qt.OtherFocusReason) if is_image: self.exec_command( 'insertHTML', '<img src="%s" alt="%s"></img>' % (prepare_string_for_xml(url, True), prepare_string_for_xml(name or _('Image'), True))) elif name: self.exec_command( 'insertHTML', '<a href="%s">%s</a>' % (prepare_string_for_xml( url, True), prepare_string_for_xml(name))) else: self.exec_command('createLink', url) else: error_dialog(self, _('Invalid URL'), _('The url %r is invalid') % link, show=True) def ask_link(self): d = QDialog(self) d.setWindowTitle(_('Create link')) l = QFormLayout() l.setFieldGrowthPolicy(QFormLayout.ExpandingFieldsGrow) d.setLayout(l) d.url = QLineEdit(d) d.name = QLineEdit(d) d.treat_as_image = QCheckBox(d) d.setMinimumWidth(600) d.bb = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) d.br = b = QPushButton(_('&Browse')) b.setIcon(QIcon(I('document_open.png'))) def cf(): files = choose_files(d, 'select link file', _('Choose file'), select_only_single_file=True) if files: path = files[0] d.url.setText(path) if path and os.path.exists(path): with lopen(path, 'rb') as f: q = what(f) is_image = q in {'jpeg', 'png', 'gif'} d.treat_as_image.setChecked(is_image) b.clicked.connect(cf) d.la = la = QLabel( _('Enter a URL. If you check the "Treat the URL as an image" box ' 'then the URL will be added as an image reference instead of as ' 'a link. You can also choose to create a link to a file on ' 'your computer. ' 'Note that if you create a link to a file on your computer, it ' 'will stop working if the file is moved.')) la.setWordWrap(True) la.setStyleSheet('QLabel { margin-bottom: 1.5ex }') l.setWidget(0, l.SpanningRole, la) l.addRow(_('Enter &URL:'), d.url) l.addRow(_('Treat the URL as an &image'), d.treat_as_image) l.addRow(_('Enter &name (optional):'), d.name) l.addRow(_('Choose a file on your computer:'), d.br) l.addRow(d.bb) d.bb.accepted.connect(d.accept) d.bb.rejected.connect(d.reject) d.resize(d.sizeHint()) link, name, is_image = None, None, False if d.exec_() == d.Accepted: link, name = unicode_type(d.url.text()).strip(), unicode_type( d.name.text()).strip() is_image = d.treat_as_image.isChecked() return link, name, is_image def parse_link(self, link): link = link.strip() if link and os.path.exists(link): return QUrl.fromLocalFile(link) has_schema = re.match(r'^[a-zA-Z]+:', link) if has_schema is not None: url = QUrl(link, QUrl.TolerantMode) if url.isValid(): return url if os.path.exists(link): return QUrl.fromLocalFile(link) if has_schema is None: first, _, rest = link.partition('.') prefix = 'http' if first == 'ftp': prefix = 'ftp' url = QUrl(prefix + '://' + link, QUrl.TolerantMode) if url.isValid(): return url return QUrl(link, QUrl.TolerantMode) def sizeHint(self): return QSize(150, 150) def exec_command(self, cmd, arg=None): frame = self.page().mainFrame() if arg is not None: js = 'document.execCommand("%s", false, %s);' % ( cmd, json.dumps(unicode_type(arg))) else: js = 'document.execCommand("%s", false, null);' % cmd frame.evaluateJavaScript(js) def remove_format_cleanup(self): self.html = self.html @property def html(self): ans = u'' try: if not self.page().mainFrame().documentElement().findFirst( 'meta[name="calibre-dont-sanitize"]').isNull(): # Bypass cleanup if special meta tag exists return unicode_type(self.page().mainFrame().toHtml()) check = unicode_type(self.page().mainFrame().toPlainText()).strip() raw = unicode_type(self.page().mainFrame().toHtml()) raw = xml_to_unicode(raw, strip_encoding_pats=True, resolve_entities=True)[0] raw = self.comments_pat.sub('', raw) if not check and '<img' not in raw.lower(): return ans try: root = html.fromstring(raw) except Exception: root = parse(raw, maybe_xhtml=False, sanitize_names=True) elems = [] for body in root.xpath('//body'): if body.text: elems.append(body.text) elems += [ html.tostring(x, encoding=unicode_type) for x in body if x.tag not in ('script', 'style') ] if len(elems) > 1: ans = u'<div>%s</div>' % (u''.join(elems)) else: ans = u''.join(elems) if not ans.startswith('<'): ans = '<p>%s</p>' % ans ans = xml_replace_entities(ans) except: import traceback traceback.print_exc() return ans @html.setter def html(self, val): if self.base_url is None: self.setHtml(val) else: self.setHtml(val, self.base_url) self.set_font_style() def set_base_url(self, qurl): self.base_url = qurl self.setHtml('', self.base_url) def set_html(self, val, allow_undo=True): if not allow_undo or self.readonly: self.html = val return mf = self.page().mainFrame() mf.evaluateJavaScript('document.execCommand("selectAll", false, null)') mf.evaluateJavaScript('document.execCommand("insertHTML", false, %s)' % json.dumps(unicode_type(val))) self.set_font_style() def set_font_style(self): fi = QFontInfo(QApplication.font(self)) f = fi.pixelSize() + 1 + int( tweaks['change_book_details_font_size_by']) fam = unicode_type(fi.family()).strip().replace('"', '') if not fam: fam = 'sans-serif' style = 'font-size: %fpx; font-family:"%s",sans-serif;' % (f, fam) # toList() is needed because PyQt on Debian is old/broken for body in self.page().mainFrame().documentElement().findAll( 'body').toList(): body.setAttribute('style', style) self.page().setContentEditable(not self.readonly) def event(self, ev): if ev.type() in (ev.KeyPress, ev.KeyRelease, ev.ShortcutOverride) and hasattr( ev, 'key') and ev.key() in (Qt.Key_Tab, Qt.Key_Escape, Qt.Key_Backtab): if (ev.key() == Qt.Key_Tab and ev.modifiers() & Qt.ControlModifier and ev.type() == ev.KeyPress): self.exec_command('insertHTML', '<span style="white-space:pre">\t</span>') ev.accept() return True ev.ignore() return False return QWebView.event(self, ev) def text(self): return self.page().selectedText() def setText(self, text): self.exec_command('insertText', text) def contextMenuEvent(self, ev): menu = self.page().createStandardContextMenu() paste = self.pageAction(QWebPage.Paste) for action in menu.actions(): if action == paste: menu.insertAction(action, self.pageAction(QWebPage.PasteAndMatchStyle)) st = self.text() if st and st.strip(): self.create_change_case_menu(menu) parent = self._parent() if hasattr(parent, 'toolbars_visible'): vis = parent.toolbars_visible menu.addAction( _('%s toolbars') % (_('Hide') if vis else _('Show')), parent.toggle_toolbars) menu.exec_(ev.globalPos())
class PluginUpdaterDialog(SizePersistedDialog): initial_extra_size = QSize(350, 100) forum_label_text = _('Plugin homepage') def __init__(self, gui, initial_filter=FILTER_UPDATE_AVAILABLE): SizePersistedDialog.__init__( self, gui, 'Plugin Updater plugin:plugin updater dialog') self.gui = gui self.forum_link = None self.zip_url = None self.model = None self.do_restart = False self._initialize_controls() self._create_context_menu() display_plugins = read_available_plugins() if display_plugins: self.model = DisplayPluginModel(display_plugins) self.proxy_model = DisplayPluginSortFilterModel(self) self.proxy_model.setSourceModel(self.model) self.plugin_view.setModel(self.proxy_model) self.plugin_view.resizeColumnsToContents() self.plugin_view.selectionModel().currentRowChanged.connect( self._plugin_current_changed) self.plugin_view.doubleClicked.connect(self.install_button.click) self.filter_combo.setCurrentIndex(initial_filter) self._select_and_focus_view() else: error_dialog(self.gui, _('Update Check Failed'), _('Unable to reach the plugin index page.'), det_msg=INDEX_URL, show=True) self.filter_combo.setEnabled(False) # Cause our dialog size to be restored from prefs or created on first usage self.resize_dialog() def _initialize_controls(self): self.setWindowTitle(_('User plugins')) self.setWindowIcon(QIcon(I('plugins/plugin_updater.png'))) layout = QVBoxLayout(self) self.setLayout(layout) title_layout = ImageTitleLayout(self, 'plugins/plugin_updater.png', _('User Plugins')) layout.addLayout(title_layout) header_layout = QHBoxLayout() layout.addLayout(header_layout) self.filter_combo = PluginFilterComboBox(self) self.filter_combo.setMinimumContentsLength(20) self.filter_combo.currentIndexChanged[int].connect( self._filter_combo_changed) header_layout.addWidget(QLabel( _('Filter list of plugins') + ':', self)) header_layout.addWidget(self.filter_combo) header_layout.addStretch(10) # filter plugins by name header_layout.addWidget(QLabel(_('Filter by name') + ':', self)) self.filter_by_name_lineedit = QLineEdit(self) self.filter_by_name_lineedit.setText("") self.filter_by_name_lineedit.textChanged.connect( self._filter_name_lineedit_changed) header_layout.addWidget(self.filter_by_name_lineedit) self.plugin_view = QTableView(self) self.plugin_view.horizontalHeader().setStretchLastSection(True) self.plugin_view.setSelectionBehavior(QAbstractItemView.SelectRows) self.plugin_view.setSelectionMode(QAbstractItemView.SingleSelection) self.plugin_view.setAlternatingRowColors(True) self.plugin_view.setSortingEnabled(True) self.plugin_view.setIconSize(QSize(28, 28)) layout.addWidget(self.plugin_view) details_layout = QHBoxLayout() layout.addLayout(details_layout) forum_label = self.forum_label = QLabel('') forum_label.setTextInteractionFlags(Qt.LinksAccessibleByMouse | Qt.LinksAccessibleByKeyboard) forum_label.linkActivated.connect(self._forum_label_activated) details_layout.addWidget(QLabel(_('Description') + ':', self), 0, Qt.AlignLeft) details_layout.addWidget(forum_label, 1, Qt.AlignRight) self.description = QLabel(self) self.description.setFrameStyle(QFrame.Panel | QFrame.Sunken) self.description.setAlignment(Qt.AlignTop | Qt.AlignLeft) self.description.setMinimumHeight(40) self.description.setWordWrap(True) layout.addWidget(self.description) self.button_box = QDialogButtonBox(QDialogButtonBox.Close) self.button_box.rejected.connect(self.reject) self.finished.connect(self._finished) self.install_button = self.button_box.addButton( _('&Install'), QDialogButtonBox.AcceptRole) self.install_button.setToolTip(_('Install the selected plugin')) self.install_button.clicked.connect(self._install_clicked) self.install_button.setEnabled(False) self.configure_button = self.button_box.addButton( ' ' + _('&Customize plugin ') + ' ', QDialogButtonBox.ResetRole) self.configure_button.setToolTip( _('Customize the options for this plugin')) self.configure_button.clicked.connect(self._configure_clicked) self.configure_button.setEnabled(False) layout.addWidget(self.button_box) def update_forum_label(self): txt = '' if self.forum_link: txt = '<a href="%s">%s</a>' % (self.forum_link, self.forum_label_text) self.forum_label.setText(txt) def _create_context_menu(self): self.plugin_view.setContextMenuPolicy(Qt.ActionsContextMenu) self.install_action = QAction( QIcon(I('plugins/plugin_upgrade_ok.png')), _('&Install'), self) self.install_action.setToolTip(_('Install the selected plugin')) self.install_action.triggered.connect(self._install_clicked) self.install_action.setEnabled(False) self.plugin_view.addAction(self.install_action) self.history_action = QAction(QIcon(I('chapters.png')), _('Version &History'), self) self.history_action.setToolTip( _('Show history of changes to this plugin')) self.history_action.triggered.connect(self._history_clicked) self.history_action.setEnabled(False) self.plugin_view.addAction(self.history_action) self.forum_action = QAction(QIcon(I('plugins/mobileread.png')), _('Plugin &Forum Thread'), self) self.forum_action.triggered.connect(self._forum_label_activated) self.forum_action.setEnabled(False) self.plugin_view.addAction(self.forum_action) sep1 = QAction(self) sep1.setSeparator(True) self.plugin_view.addAction(sep1) self.toggle_enabled_action = QAction(_('Enable/&Disable plugin'), self) self.toggle_enabled_action.setToolTip( _('Enable or disable this plugin')) self.toggle_enabled_action.triggered.connect( self._toggle_enabled_clicked) self.toggle_enabled_action.setEnabled(False) self.plugin_view.addAction(self.toggle_enabled_action) self.uninstall_action = QAction(_('&Remove plugin'), self) self.uninstall_action.setToolTip(_('Uninstall the selected plugin')) self.uninstall_action.triggered.connect(self._uninstall_clicked) self.uninstall_action.setEnabled(False) self.plugin_view.addAction(self.uninstall_action) sep2 = QAction(self) sep2.setSeparator(True) self.plugin_view.addAction(sep2) self.donate_enabled_action = QAction(QIcon(I('donate.png')), _('Donate to developer'), self) self.donate_enabled_action.setToolTip( _('Donate to the developer of this plugin')) self.donate_enabled_action.triggered.connect(self._donate_clicked) self.donate_enabled_action.setEnabled(False) self.plugin_view.addAction(self.donate_enabled_action) sep3 = QAction(self) sep3.setSeparator(True) self.plugin_view.addAction(sep3) self.configure_action = QAction(QIcon(I('config.png')), _('&Customize plugin'), self) self.configure_action.setToolTip( _('Customize the options for this plugin')) self.configure_action.triggered.connect(self._configure_clicked) self.configure_action.setEnabled(False) self.plugin_view.addAction(self.configure_action) def _finished(self, *args): if self.model: update_plugins = filter(filter_upgradeable_plugins, self.model.display_plugins) self.gui.recalc_update_label(len(update_plugins)) def _plugin_current_changed(self, current, previous): if current.isValid(): actual_idx = self.proxy_model.mapToSource(current) display_plugin = self.model.display_plugins[actual_idx.row()] self.description.setText(display_plugin.description) self.forum_link = display_plugin.forum_link self.zip_url = display_plugin.zip_url self.forum_action.setEnabled(bool(self.forum_link)) self.install_button.setEnabled( display_plugin.is_valid_to_install()) self.install_action.setEnabled(self.install_button.isEnabled()) self.uninstall_action.setEnabled(display_plugin.is_installed()) self.history_action.setEnabled(display_plugin.has_changelog) self.configure_button.setEnabled(display_plugin.is_installed()) self.configure_action.setEnabled(self.configure_button.isEnabled()) self.toggle_enabled_action.setEnabled( display_plugin.is_installed()) self.donate_enabled_action.setEnabled( bool(display_plugin.donation_link)) else: self.description.setText('') self.forum_link = None self.zip_url = None self.forum_action.setEnabled(False) self.install_button.setEnabled(False) self.install_action.setEnabled(False) self.uninstall_action.setEnabled(False) self.history_action.setEnabled(False) self.configure_button.setEnabled(False) self.configure_action.setEnabled(False) self.toggle_enabled_action.setEnabled(False) self.donate_enabled_action.setEnabled(False) self.update_forum_label() def _donate_clicked(self): plugin = self._selected_display_plugin() if plugin and plugin.donation_link: open_url(QUrl(plugin.donation_link)) def _select_and_focus_view(self, change_selection=True): if change_selection and self.plugin_view.model().rowCount() > 0: self.plugin_view.selectRow(0) else: idx = self.plugin_view.selectionModel().currentIndex() self._plugin_current_changed(idx, 0) self.plugin_view.setFocus() def _filter_combo_changed(self, idx): self.filter_by_name_lineedit.setText( "" ) # clear the name filter text when a different group was selected self.proxy_model.set_filter_criteria(idx) if idx == FILTER_NOT_INSTALLED: self.plugin_view.sortByColumn(5, Qt.DescendingOrder) else: self.plugin_view.sortByColumn(0, Qt.AscendingOrder) self._select_and_focus_view() def _filter_name_lineedit_changed(self, text): self.proxy_model.set_filter_text( text) # set the filter text for filterAcceptsRow def _forum_label_activated(self): if self.forum_link: open_url(QUrl(self.forum_link)) def _selected_display_plugin(self): idx = self.plugin_view.selectionModel().currentIndex() actual_idx = self.proxy_model.mapToSource(idx) return self.model.display_plugins[actual_idx.row()] def _uninstall_plugin(self, name_to_remove): if DEBUG: prints('Removing plugin: ', name_to_remove) remove_plugin(name_to_remove) # Make sure that any other plugins that required this plugin # to be uninstalled first have the requirement removed for display_plugin in self.model.display_plugins: # Make sure we update the status and display of the # plugin we just uninstalled if name_to_remove in display_plugin.uninstall_plugins: if DEBUG: prints('Removing uninstall dependency for: ', display_plugin.name) display_plugin.uninstall_plugins.remove(name_to_remove) if display_plugin.name == name_to_remove: if DEBUG: prints('Resetting plugin to uninstalled status: ', display_plugin.name) display_plugin.installed_version = None display_plugin.plugin = None display_plugin.uninstall_plugins = [] if self.proxy_model.filter_criteria not in [ FILTER_INSTALLED, FILTER_UPDATE_AVAILABLE ]: self.model.refresh_plugin(display_plugin) def _uninstall_clicked(self): display_plugin = self._selected_display_plugin() if not question_dialog( self, _('Are you sure?'), '<p>' + _('Are you sure you want to uninstall the <b>%s</b> plugin?') % display_plugin.name, show_copy_button=False): return self._uninstall_plugin(display_plugin.name) if self.proxy_model.filter_criteria in [ FILTER_INSTALLED, FILTER_UPDATE_AVAILABLE ]: self.model.beginResetModel(), self.model.endResetModel() self._select_and_focus_view() else: self._select_and_focus_view(change_selection=False) def _install_clicked(self): display_plugin = self._selected_display_plugin() if not question_dialog( self, _('Install %s') % display_plugin.name, '<p>' + _('Installing plugins is a <b>security risk</b>. ' 'Plugins can contain a virus/malware. ' 'Only install it if you got it from a trusted source.' ' Are you sure you want to proceed?'), show_copy_button=False): return if display_plugin.uninstall_plugins: uninstall_names = list(display_plugin.uninstall_plugins) if DEBUG: prints('Uninstalling plugin: ', ', '.join(uninstall_names)) for name_to_remove in uninstall_names: self._uninstall_plugin(name_to_remove) plugin_zip_url = display_plugin.zip_url if DEBUG: prints('Downloading plugin zip attachment: ', plugin_zip_url) self.gui.status_bar.showMessage( _('Downloading plugin zip attachment: %s') % plugin_zip_url) zip_path = self._download_zip(plugin_zip_url) if DEBUG: prints('Installing plugin: ', zip_path) self.gui.status_bar.showMessage(_('Installing plugin: %s') % zip_path) do_restart = False try: from calibre.customize.ui import config installed_plugins = frozenset(config['plugins']) try: plugin = add_plugin(zip_path) except NameConflict as e: return error_dialog(self.gui, _('Already exists'), unicode(e), show=True) # Check for any toolbars to add to. widget = ConfigWidget(self.gui) widget.gui = self.gui widget.check_for_add_to_toolbars(plugin, previously_installed=plugin.name in installed_plugins) self.gui.status_bar.showMessage( _('Plugin installed: %s') % display_plugin.name) d = info_dialog( self.gui, _('Success'), _('Plugin <b>{0}</b> successfully installed under <b>' ' {1} plugins</b>. You may have to restart calibre ' 'for the plugin to take effect.').format( plugin.name, plugin.type), show_copy_button=False) b = d.bb.addButton(_('Restart calibre now'), d.bb.AcceptRole) b.setIcon(QIcon(I('lt.png'))) d.do_restart = False def rf(): d.do_restart = True b.clicked.connect(rf) d.set_details('') d.exec_() b.clicked.disconnect() do_restart = d.do_restart display_plugin.plugin = plugin # We cannot read the 'actual' version information as the plugin will not be loaded yet display_plugin.installed_version = display_plugin.available_version except: if DEBUG: prints('ERROR occurred while installing plugin: %s' % display_plugin.name) traceback.print_exc() error_dialog( self.gui, _('Install Plugin Failed'), _('A problem occurred while installing this plugin.' ' This plugin will now be uninstalled.' ' Please post the error message in details below into' ' the forum thread for this plugin and restart Calibre.'), det_msg=traceback.format_exc(), show=True) if DEBUG: prints('Due to error now uninstalling plugin: %s' % display_plugin.name) remove_plugin(display_plugin.name) display_plugin.plugin = None display_plugin.uninstall_plugins = [] if self.proxy_model.filter_criteria in [ FILTER_NOT_INSTALLED, FILTER_UPDATE_AVAILABLE ]: self.model.beginResetModel(), self.model.endResetModel() self._select_and_focus_view() else: self.model.refresh_plugin(display_plugin) self._select_and_focus_view(change_selection=False) if do_restart: self.do_restart = True self.accept() def _history_clicked(self): display_plugin = self._selected_display_plugin() text = self._read_version_history_html(display_plugin.forum_link) if text: dlg = VersionHistoryDialog(self, display_plugin.name, text) dlg.exec_() else: return error_dialog( self, _('Version history missing'), _('Unable to find the version history for %s') % display_plugin.name, show=True) def _configure_clicked(self): display_plugin = self._selected_display_plugin() plugin = display_plugin.plugin if not plugin.is_customizable(): return info_dialog(self, _('Plugin not customizable'), _('Plugin: %s does not need customization') % plugin.name, show=True) from calibre.customize import InterfaceActionBase if isinstance(plugin, InterfaceActionBase) and not getattr( plugin, 'actual_iaction_plugin_loaded', False): return error_dialog(self, _('Must restart'), _('You must restart calibre before you can' ' configure the <b>%s</b> plugin') % plugin.name, show=True) plugin.do_user_config(self.parent()) def _toggle_enabled_clicked(self): display_plugin = self._selected_display_plugin() plugin = display_plugin.plugin if not plugin.can_be_disabled: return error_dialog(self, _('Plugin cannot be disabled'), _('The plugin: %s cannot be disabled') % plugin.name, show=True) if is_disabled(plugin): enable_plugin(plugin) else: disable_plugin(plugin) self.model.refresh_plugin(display_plugin) def _read_version_history_html(self, forum_link): br = browser() br.set_handle_gzip(True) try: raw = br.open_novisit(forum_link).read() if not raw: return None except: traceback.print_exc() return None raw = raw.decode('utf-8', errors='replace') root = html.fromstring(raw) spoiler_nodes = root.xpath( '//div[@class="smallfont" and strong="Spoiler"]') for spoiler_node in spoiler_nodes: try: if spoiler_node.getprevious() is None: # This is a spoiler node that has been indented using [INDENT] # Need to go up to parent div, then previous node to get header heading_node = spoiler_node.getparent().getprevious() else: # This is a spoiler node after a BR tag from the heading heading_node = spoiler_node.getprevious().getprevious() if heading_node is None: continue if heading_node.text_content().lower().find( 'version history') != -1: div_node = spoiler_node.xpath('div')[0] text = html.tostring(div_node, method='html', encoding=unicode) return re.sub('<div\s.*?>', '<div>', text) except: if DEBUG: prints('======= MobileRead Parse Error =======') traceback.print_exc() prints(html.tostring(spoiler_node)) return None def _download_zip(self, plugin_zip_url): from calibre.ptempfile import PersistentTemporaryFile br = browser(user_agent='%s %s' % (__appname__, __version__)) raw = br.open_novisit(plugin_zip_url).read() with PersistentTemporaryFile('.zip') as pt: pt.write(raw) return pt.name
class EditorWidget(QWebView): # {{{ def __init__(self, parent=None): QWebView.__init__(self, parent) self._parent = weakref.ref(parent) self.readonly = False self.comments_pat = re.compile(r'<!--.*?-->', re.DOTALL) extra_shortcuts = { 'ToggleBold': 'Bold', 'ToggleItalic': 'Italic', 'ToggleUnderline': 'Underline', } for wac, name, icon, text, checkable in [ ('ToggleBold', 'bold', 'format-text-bold', _('Bold'), True), ('ToggleItalic', 'italic', 'format-text-italic', _('Italic'), True), ('ToggleUnderline', 'underline', 'format-text-underline', _('Underline'), True), ('ToggleStrikethrough', 'strikethrough', 'format-text-strikethrough', _('Strikethrough'), True), ('ToggleSuperscript', 'superscript', 'format-text-superscript', _('Superscript'), True), ('ToggleSubscript', 'subscript', 'format-text-subscript', _('Subscript'), True), ('InsertOrderedList', 'ordered_list', 'format-list-ordered', _('Ordered list'), True), ('InsertUnorderedList', 'unordered_list', 'format-list-unordered', _('Unordered list'), True), ('AlignLeft', 'align_left', 'format-justify-left', _('Align left'), False), ('AlignCenter', 'align_center', 'format-justify-center', _('Align center'), False), ('AlignRight', 'align_right', 'format-justify-right', _('Align right'), False), ('AlignJustified', 'align_justified', 'format-justify-fill', _('Align justified'), False), ('Undo', 'undo', 'edit-undo', _('Undo'), False), ('Redo', 'redo', 'edit-redo', _('Redo'), False), ('RemoveFormat', 'remove_format', 'trash', _('Remove formatting'), False), ('Copy', 'copy', 'edit-copy', _('Copy'), False), ('Paste', 'paste', 'edit-paste', _('Paste'), False), ('Cut', 'cut', 'edit-cut', _('Cut'), False), ('Indent', 'indent', 'format-indent-more', _('Increase Indentation'), False), ('Outdent', 'outdent', 'format-indent-less', _('Decrease Indentation'), False), ('SelectAll', 'select_all', 'edit-select-all', _('Select all'), False), ]: ac = PageAction(wac, icon, text, checkable, self) setattr(self, 'action_'+name, ac) ss = extra_shortcuts.get(wac, None) if ss: ac.setShortcut(QKeySequence(getattr(QKeySequence, ss))) if wac == 'RemoveFormat': ac.triggered.connect(self.remove_format_cleanup, type=Qt.QueuedConnection) self.action_color = QAction(QIcon(I('format-text-color.png')), _('Foreground color'), self) self.action_color.triggered.connect(self.foreground_color) self.action_background = QAction(QIcon(I('format-fill-color.png')), _('Background color'), self) self.action_background.triggered.connect(self.background_color) self.action_block_style = QAction(QIcon(I('format-text-heading.png')), _('Style text block'), self) self.action_block_style.setToolTip( _('Style the selected text block')) self.block_style_menu = QMenu(self) self.action_block_style.setMenu(self.block_style_menu) self.block_style_actions = [] for text, name in [ (_('Normal'), 'p'), (_('Heading') +' 1', 'h1'), (_('Heading') +' 2', 'h2'), (_('Heading') +' 3', 'h3'), (_('Heading') +' 4', 'h4'), (_('Heading') +' 5', 'h5'), (_('Heading') +' 6', 'h6'), (_('Pre-formatted'), 'pre'), (_('Blockquote'), 'blockquote'), (_('Address'), 'address'), ]: ac = BlockStyleAction(text, name, self) self.block_style_menu.addAction(ac) self.block_style_actions.append(ac) self.action_insert_link = QAction(QIcon(I('insert-link.png')), _('Insert link or image'), self) self.action_insert_link.triggered.connect(self.insert_link) self.pageAction(QWebPage.ToggleBold).changed.connect(self.update_link_action) self.action_insert_link.setEnabled(False) self.action_clear = QAction(QIcon(I('edit-clear.png')), _('Clear'), self) self.action_clear.triggered.connect(self.clear_text) self.page().setLinkDelegationPolicy(QWebPage.DelegateAllLinks) self.page().linkClicked.connect(self.link_clicked) self.setHtml('') self.set_readonly(False) def update_link_action(self): wac = self.pageAction(QWebPage.ToggleBold) self.action_insert_link.setEnabled(wac.isEnabled()) def set_readonly(self, what): self.readonly = what self.page().setContentEditable(not self.readonly) def clear_text(self, *args): us = self.page().undoStack() us.beginMacro('clear all text') self.action_select_all.trigger() self.action_remove_format.trigger() self.exec_command('delete') us.endMacro() self.set_font_style() self.setFocus(Qt.OtherFocusReason) def link_clicked(self, url): open_url(url) def foreground_color(self): col = QColorDialog.getColor(Qt.black, self, _('Choose foreground color'), QColorDialog.ShowAlphaChannel) if col.isValid(): self.exec_command('foreColor', unicode(col.name())) def background_color(self): col = QColorDialog.getColor(Qt.white, self, _('Choose background color'), QColorDialog.ShowAlphaChannel) if col.isValid(): self.exec_command('hiliteColor', unicode(col.name())) def insert_link(self, *args): link, name, is_image = self.ask_link() if not link: return url = self.parse_link(link) if url.isValid(): url = unicode(url.toString(QUrl.None)) self.setFocus(Qt.OtherFocusReason) if is_image: self.exec_command('insertHTML', '<img src="%s" alt="%s"></img>'%(prepare_string_for_xml(url, True), prepare_string_for_xml(name or _('Image'), True))) elif name: self.exec_command('insertHTML', '<a href="%s">%s</a>'%(prepare_string_for_xml(url, True), prepare_string_for_xml(name))) else: self.exec_command('createLink', url) else: error_dialog(self, _('Invalid URL'), _('The url %r is invalid') % link, show=True) def ask_link(self): d = QDialog(self) d.setWindowTitle(_('Create link')) l = QFormLayout() d.setLayout(l) d.url = QLineEdit(d) d.name = QLineEdit(d) d.treat_as_image = QCheckBox(d) d.setMinimumWidth(600) d.bb = QDialogButtonBox(QDialogButtonBox.Ok|QDialogButtonBox.Cancel) d.br = b = QPushButton(_('&Browse')) b.setIcon(QIcon(I('document_open.png'))) def cf(): files = choose_files(d, 'select link file', _('Choose file'), select_only_single_file=True) if files: path = files[0] d.url.setText(path) if path and os.path.exists(path): with lopen(path, 'rb') as f: q = what(f) is_image = q in {'jpeg', 'png', 'gif'} d.treat_as_image.setChecked(is_image) b.clicked.connect(cf) d.la = la = QLabel(_( 'Enter a URL. If you check the "Treat the URL as an image" box ' 'then the URL will be added as an image reference instead of as ' 'a link. You can also choose to create a link to a file on ' 'your computer. ' 'Note that if you create a link to a file on your computer, it ' 'will stop working if the file is moved.')) la.setWordWrap(True) la.setStyleSheet('QLabel { margin-bottom: 1.5ex }') l.setWidget(0, l.SpanningRole, la) l.addRow(_('Enter &URL:'), d.url) l.addRow(_('Treat the URL as an &image'), d.treat_as_image) l.addRow(_('Enter &name (optional):'), d.name) l.addRow(_('Choose a file on your computer:'), d.br) l.addRow(d.bb) d.bb.accepted.connect(d.accept) d.bb.rejected.connect(d.reject) d.resize(d.sizeHint()) link, name, is_image = None, None, False if d.exec_() == d.Accepted: link, name = unicode(d.url.text()).strip(), unicode(d.name.text()).strip() is_image = d.treat_as_image.isChecked() return link, name, is_image def parse_link(self, link): link = link.strip() if link and os.path.exists(link): return QUrl.fromLocalFile(link) has_schema = re.match(r'^[a-zA-Z]+:', link) if has_schema is not None: url = QUrl(link, QUrl.TolerantMode) if url.isValid(): return url if os.path.exists(link): return QUrl.fromLocalFile(link) if has_schema is None: first, _, rest = link.partition('.') prefix = 'http' if first == 'ftp': prefix = 'ftp' url = QUrl(prefix +'://'+link, QUrl.TolerantMode) if url.isValid(): return url return QUrl(link, QUrl.TolerantMode) def sizeHint(self): return QSize(150, 150) def exec_command(self, cmd, arg=None): frame = self.page().mainFrame() if arg is not None: js = 'document.execCommand("%s", false, %s);' % (cmd, json.dumps(unicode(arg))) else: js = 'document.execCommand("%s", false, null);' % cmd frame.evaluateJavaScript(js) def remove_format_cleanup(self): self.html = self.html @dynamic_property def html(self): def fget(self): ans = u'' try: if not self.page().mainFrame().documentElement().findFirst('meta[name="calibre-dont-sanitize"]').isNull(): # Bypass cleanup if special meta tag exists return unicode(self.page().mainFrame().toHtml()) check = unicode(self.page().mainFrame().toPlainText()).strip() raw = unicode(self.page().mainFrame().toHtml()) raw = xml_to_unicode(raw, strip_encoding_pats=True, resolve_entities=True)[0] raw = self.comments_pat.sub('', raw) if not check and '<img' not in raw.lower(): return ans try: root = html.fromstring(raw) except: root = fromstring(raw) elems = [] for body in root.xpath('//body'): if body.text: elems.append(body.text) elems += [html.tostring(x, encoding=unicode) for x in body if x.tag not in ('script', 'style')] if len(elems) > 1: ans = u'<div>%s</div>'%(u''.join(elems)) else: ans = u''.join(elems) if not ans.startswith('<'): ans = '<p>%s</p>'%ans ans = xml_replace_entities(ans) except: import traceback traceback.print_exc() return ans def fset(self, val): self.setHtml(val) self.set_font_style() return property(fget=fget, fset=fset) def set_html(self, val, allow_undo=True): if not allow_undo or self.readonly: self.html = val return mf = self.page().mainFrame() mf.evaluateJavaScript('document.execCommand("selectAll", false, null)') mf.evaluateJavaScript('document.execCommand("insertHTML", false, %s)' % json.dumps(unicode(val))) self.set_font_style() def set_font_style(self): fi = QFontInfo(QApplication.font(self)) f = fi.pixelSize() + 1 + int(tweaks['change_book_details_font_size_by']) fam = unicode(fi.family()).strip().replace('"', '') if not fam: fam = 'sans-serif' style = 'font-size: %fpx; font-family:"%s",sans-serif;' % (f, fam) # toList() is needed because PyQt on Debian is old/broken for body in self.page().mainFrame().documentElement().findAll('body').toList(): body.setAttribute('style', style) self.page().setContentEditable(not self.readonly) def event(self, ev): if ev.type() in (ev.KeyPress, ev.KeyRelease, ev.ShortcutOverride) and ev.key() in ( Qt.Key_Tab, Qt.Key_Escape, Qt.Key_Backtab): if (ev.key() == Qt.Key_Tab and ev.modifiers() & Qt.ControlModifier and ev.type() == ev.KeyPress): self.exec_command('insertHTML', '<span style="white-space:pre">\t</span>') ev.accept() return True ev.ignore() return False return QWebView.event(self, ev) def contextMenuEvent(self, ev): menu = self.page().createStandardContextMenu() paste = self.pageAction(QWebPage.Paste) for action in menu.actions(): if action == paste: menu.insertAction(action, self.pageAction(QWebPage.PasteAndMatchStyle)) parent = self._parent() if hasattr(parent, 'toolbars_visible'): vis = parent.toolbars_visible menu.addAction(_('%s toolbars') % (_('Hide') if vis else _('Show')), (parent.hide_toolbars if vis else parent.show_toolbars)) menu.exec_(ev.globalPos())
def rebuild_menus(self): fav_menus = cfg.plugin_prefs[cfg.STORE_MENUS] m = self.menu m.clear() in_device_mode = self.gui.location_manager.has_device discovered_plugins = {} for fav_menu in fav_menus: if fav_menu is None: m.addSeparator() continue ac = None paths = list(fav_menu['path']) plugin_name = paths[0] is_device_only_plugin = False if plugin_name == 'Location Manager': # Special case handling since not iaction instances is_device_only_plugin = True paths = paths[1:] for loc_action in self.gui.location_manager.all_actions[1:]: if unicode(loc_action.text()) == paths[0]: if len(paths) > 1: # This is an action on the menu for this plugin or its submenus ac = self._find_action_for_menu( loc_action.menu(), paths[1:], plugin_name) else: # This is a top-level plugin being added to the menu ac = loc_action break else: iaction = self.gui.iactions.get(plugin_name, None) if iaction is not None: if iaction not in discovered_plugins: discovered_plugins[iaction] = True if hasattr(iaction, 'menu'): iaction.menu.aboutToShow.emit() is_device_only_plugin = 'toolbar' in iaction.dont_add_to and 'toolbar-device' not in iaction.dont_add_to if len(paths) > 1: # This is an action on the menu for this plugin or its submenus ac = self._find_action_for_menu( iaction.qaction.menu(), paths[1:], plugin_name) else: # This is a top-level plugin being added to the menu ac = iaction.qaction if ac is None: # We have a menu action that is not available. Perhaps the user # has switched libraries, uninstalled a plugin or for some other # reason that underlying item is not available any more. We still add # a placeholder menu item, but will have no icon and be disabled. mac = QAction(fav_menu['display'], m) mac.setEnabled(False) #print('Favourite Menu: action not found:', fav_menu) else: # We have found the underlying action for this menu item. # Clone the original QAction in order to alias the text for it mac = ActionWrapper(ac, m) mac.setText(fav_menu['display']) if is_device_only_plugin and not in_device_mode: mac.setEnabled(False) m.addAction(mac) m.addSeparator() create_menu_action_unique(self, m, _('&Customize plugin') + '...', 'config.png', shortcut=False, triggered=self.show_configuration)
class EKWindow(QDialog): """ Class which is responisble for running this entire application """ def __init__(self): """ Constructor for this class """ super(EKWindow, self).__init__() self.engine = Engine("tables/Tamil-bamini.txt.in") # Settings file initialization self.settingsFilePath = os.getenv("APPDATA") + "\\" + qApp.applicationName() + "\eksettings.ini" self.init_settings() # Function to check whether the settings file is or not. self.iniSettings = QSettings(self.settingsFilePath, QSettings.IniFormat) # Variable Initialization self.registrySettings = QSettings("HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run", QSettings.NativeFormat) self.shortcutModifierKey = self.iniSettings.value("shortcut_modifier") self.shortcutKey = self.iniSettings.value("shortcut") self.selectedKeyboard = self.iniSettings.value("selected_keyboard") self.keyboardStatus = False self.fileName = "" # Ui variable Initialization self.iconGroupBox = QGroupBox("Keyboards") self.iconLabel = QLabel("Keyboard:") self.iconComboBox = QComboBox(self) self.shortcutGroupBox = QGroupBox("Shortcut Setting") self.shortcutComboBox1 = QComboBox(self) self.shortcutComboBox2 = QComboBox(self) self.otherSettingsGroupBox = QGroupBox("Other Settings") self.checkboxStartWithWindows = QCheckBox() self.minimizeAction = QAction("Minimize", self) self.maximizeAction = QAction("Maximize", self) self.settingsAction = QAction("Settings", self) self.aboutAction = QAction("About", self) self.quitAction = QAction("Quit", self) self.trayIconMenu = QMenu(self) self.trayIcon = QSystemTrayIcon(self) self.mainLayout = QVBoxLayout() self.mainLayout.addWidget(self.iconGroupBox) self.mainLayout.addWidget(self.shortcutGroupBox) self.mainLayout.addWidget(self.otherSettingsGroupBox) self.setLayout(self.mainLayout) # UI constructor and connectors self.create_settings_group_boxes() self.create_actions() self.create_tray_icon() # Signal connectors self.iconComboBox.currentIndexChanged.connect(self.change_keyboard) self.shortcutComboBox1.currentIndexChanged.connect(self.set_shortcut_modifier) self.shortcutComboBox2.currentIndexChanged.connect(self.set_shortcut_key) self.trayIcon.activated.connect(self.icon_activated) self.checkboxStartWithWindows.stateChanged.connect(self.checkbox_start_with_windows_ticked) if self.keyboardStatus: self.iconComboBox.setCurrentIndex(self.selectedKeyBoard) else: self.change_keyboard(0) self.iconComboBox.setCurrentIndex(0) self.trayIcon.show() self.set_shortcut_key() self.setWindowTitle(qApp.applicationName() + " " + qApp.applicationVersion()) def init_settings(self): """ Function to check whether the settings file is there or not. If there is no file, then it will create with default settings. """ if not os.path.exists(self.settingsFilePath): settings_dir = os.getenv("APPDATA") + "\\" + qApp.applicationName() if not os.path.exists(settings_dir): os.makedirs(settings_dir) setting_path = "" if getattr(sys, 'frozen', False): setting_path = os.path.dirname(sys.executable) elif __file__: setting_path = os.path.dirname(__file__) shutil.copyfile(os.path.join(setting_path, "resources\eksettings.ini"), self.settingsFilePath) return def create_settings_group_boxes(self): """ UI generator function. """ self.iconComboBox.addItem("No Keyboard") self.iconComboBox.addItem("Tamil99") self.iconComboBox.addItem("Phonetic") self.iconComboBox.addItem("Typewriter") self.iconComboBox.addItem("Bamini") self.iconComboBox.addItem("Inscript") icon_layout = QHBoxLayout(self) icon_layout.addWidget(self.iconLabel) icon_layout.addWidget(self.iconComboBox) icon_layout.addStretch() self.iconGroupBox.setLayout(icon_layout) shortcut_label_1 = QLabel("Modifier Key:") shortcut_label_2 = QLabel("Shortcut Key:") self.shortcutComboBox1.addItem("NONE") self.shortcutComboBox1.addItem("CTRL") self.shortcutComboBox1.addItem("ALT") modifier_index = self.shortcutComboBox1.findText(self.shortcutModifierKey) self.shortcutComboBox1.setCurrentIndex(modifier_index) self.shortcutComboBox2.setMinimumContentsLength(3) if modifier_index == 0: self.shortcutComboBox2.addItem("F1") self.shortcutComboBox2.addItem("ESC") self.shortcutComboBox2.addItem("F2") self.shortcutComboBox2.addItem("F3") self.shortcutComboBox2.addItem("F4") self.shortcutComboBox2.addItem("F5") self.shortcutComboBox2.addItem("F6") self.shortcutComboBox2.addItem("F7") self.shortcutComboBox2.addItem("F8") self.shortcutComboBox2.addItem("F9") self.shortcutComboBox2.addItem("F10") else: self.shortcutComboBox2.addItem("1") self.shortcutComboBox2.addItem("2") self.shortcutComboBox2.addItem("3") self.shortcutComboBox2.addItem("4") self.shortcutComboBox2.addItem("5") self.shortcutComboBox2.addItem("6") self.shortcutComboBox2.addItem("7") self.shortcutComboBox2.addItem("8") self.shortcutComboBox2.addItem("9") self.shortcutComboBox2.addItem("0") key_index = self.shortcutComboBox2.findText(self.shortcutKey) self.shortcutComboBox2.setCurrentIndex(key_index) shortcut_layout = QHBoxLayout(self) shortcut_layout.addWidget(shortcut_label_1) shortcut_layout.addWidget(self.shortcutComboBox1) shortcut_layout.addWidget(shortcut_label_2) shortcut_layout.addWidget(self.shortcutComboBox2) shortcut_layout.addStretch() self.shortcutGroupBox.setLayout(shortcut_layout) checkbox_start_with_windows_label = QLabel("Start eKalappai whenever windows starts") # if registry entry for auto start with windows for the current user exists, then check the checkbox if self.registrySettings.contains(qApp.applicationName()): self.checkboxStartWithWindows.setChecked(True) else: self.checkboxStartWithWindows.setChecked(False) other_settings_layout = QHBoxLayout(self) other_settings_layout.addWidget(checkbox_start_with_windows_label) other_settings_layout.addWidget(self.checkboxStartWithWindows) other_settings_layout.addStretch() self.otherSettingsGroupBox.setLayout(other_settings_layout) def set_shortcut_key(self): """ Function to change the shortcut key when its changed. """ self.shortcutKey = self.shortcutComboBox2.currentText() self.iniSettings.setValue("shortcut", self.shortcutKey) self.register_shortcut_listener() if self.shortcutKey == "ESC": self.shortcutKeyHex = 0x1B elif self.shortcutKey == "F1": self.shortcutKeyHex = 0x70 elif self.shortcutKey == "F2": self.shortcutKeyHex = 0x71 elif self.shortcutKey == "F3": self.shortcutKeyHex = 0x72 elif self.shortcutKey == "F4": self.shortcutKeyHex = 0x73 elif self.shortcutKey == "F5": self.shortcutKeyHex = 0x74 elif self.shortcutKey == "F6": self.shortcutKeyHex = 0x75 elif self.shortcutKey == "F7": self.shortcutKeyHex = 0x76 elif self.shortcutKey == "F8": self.shortcutKeyHex = 0x77 elif self.shortcutKey == "F9": self.shortcutKeyHex = 0x78 elif self.shortcutKey == "F10": self.shortcutKeyHex = 0x79 elif self.shortcutKey == "1": self.shortcutKeyHex = 0x31 elif self.shortcutKey == "2": self.shortcutKeyHex = 0x32 elif self.shortcutKey == "3": self.shortcutKeyHex = 0x33 elif self.shortcutKey == "4": self.shortcutKeyHex = 0x34 elif self.shortcutKey == "5": self.shortcutKeyHex = 0x35 elif self.shortcutKey == "6": self.shortcutKeyHex = 0x36 elif self.shortcutKey == "7": self.shortcutKeyHex = 0x37 elif self.shortcutKey == "8": self.shortcutKeyHex = 0x38 elif self.shortcutKey == "9": self.shortcutKeyHex = 0x39 elif self.shortcutKey == "0": self.shortcutKeyHex = 0x30 def create_actions(self): """ Slot connectors for all right clicking and other actions. """ self.minimizeAction.triggered.connect(self.hide) self.maximizeAction.triggered.connect(self.showMaximized) self.settingsAction.triggered.connect(self.showNormal) self.aboutAction.triggered.connect(self.show_about) self.quitAction.triggered.connect(self.quit) def quit(self): self.engine.un_hook() exit(0) def create_tray_icon(self): """ Tray icon creator and corresponding connectors """ self.trayIconMenu.addAction(self.settingsAction) self.trayIconMenu.addSeparator() self.trayIconMenu.addAction(self.aboutAction) self.trayIconMenu.addSeparator() self.trayIconMenu.addAction(self.quitAction) self.trayIcon.setContextMenu(self.trayIconMenu) def setVisible(self, visible): self.settingsAction.setEnabled(self.isMaximized() or not visible) super(EKWindow, self).setVisible(visible) def closeEvent(self, event): if self.trayIcon.isVisible(): self.hide() event.ignore() def load_keyboard(self): """ Mapping file loading function """ if self.selectedKeyboard == 1: self.fileName = "tables/Tamil-tamil99.txt.in" elif self.selectedKeyboard == 2: self.fileName = "tables/Tamil-phonetic.txt.in" elif self.selectedKeyboard == 3: self.fileName = "tables/Tamil-typewriter.txt.in" elif self.selectedKeyboard == 4: self.fileName = "tables/Tamil-bamini.txt.in" elif self.selectedKeyboard == 5: self.fileName = "tables/Tamil-inscript.txt.in" else: pass def getPath(self, index): if index == 1: self.path = "tables/Tamil-tamil99.txt.in" elif index == 2: self.path = "tables/Tamil-phonetic.txt.in" elif index == 3: self.path = "tables/Tamil-typewriter.txt.in" elif index == 4: self.path = "tables/Tamil-bamini.txt.in" elif index == 5: self.path = "tables/Tamil-inscript.txt.in" else: pass def change_keyboard(self, index): """ Function to change the keyboard based on the index which was sent as a param """ if int(index) != 0: self.iniSettings.setValue("selected_keyboard", index) self.selectedKeyboard = index self.iconComboBox.setCurrentIndex(int(index)) icon = self.iconComboBox.itemIcon(int(index)) self.trayIcon.setIcon(icon) self.setWindowIcon(icon) self.trayIcon.setToolTip(self.iconComboBox.itemText(int(index))) self.show_tray_message(index) self.load_keyboard() if int(index) != 0: self.getPath(int(index)) self.engine.file_name = self.path self.engine.initialize() self.engine.conv_state = True else: try: self.engine.conv_state = False except: pass def icon_activated(self, reason): """ Function to toggle the state when the icon is clicked or shortcut key is pressed """ if reason == QSystemTrayIcon.DoubleClick: pass elif reason == QSystemTrayIcon.Trigger: if self.keyboardStatus: self.keyboardStatus = False else: self.keyboardStatus = True if self.keyboardStatus: self.change_keyboard(self.selectedKeyboard) else: self.change_keyboard(0) elif reason == QSystemTrayIcon.MiddleClick: pass else: pass def show_tray_message(self, index): """ Tray message generator when there is change in keyboard state """ icon = QSystemTrayIcon.MessageIcon(0) message = self.iconComboBox.itemText(int(index)) + " set" self.trayIcon.showMessage(qApp.applicationName() + " " + qApp.applicationVersion(), message, icon, 100) def checkbox_start_with_windows_ticked(self): """ Function to add or disable registry entry to auto start ekalappai with windows for the current users """ if self.checkboxStartWithWindows.isChecked(): self.registrySettings.setValue(qApp.applicationName(), qApp.applicationFilePath()) else: self.registrySettings.remove(qApp.applicationName()) def show_about(self): pass def set_shortcut_modifier(self, index): """ Function to set the shortcut modifier when its changed. """ self.iniSettings.setValue("shortcut_modifier", self.shortcutComboBox1.currentText()) self.shortcutModifierKey = self.iniSettings.value("shortcut_modifier") # if none is selected, the allowed single key shortcuts should change if index == 0: self.shortcutComboBox2.clear() self.shortcutComboBox2.addItem("ESC") self.shortcutComboBox2.addItem("F1") self.shortcutComboBox2.addItem("F2") self.shortcutComboBox2.addItem("F3") self.shortcutComboBox2.addItem("F4") self.shortcutComboBox2.addItem("F5") self.shortcutComboBox2.addItem("F6") self.shortcutComboBox2.addItem("F7") self.shortcutComboBox2.addItem("F8") self.shortcutComboBox2.addItem("F9") self.shortcutComboBox2.addItem("F10") else: self.shortcutComboBox2.clear() self.shortcutComboBox2.addItem("1") self.shortcutComboBox2.addItem("2") self.shortcutComboBox2.addItem("3") self.shortcutComboBox2.addItem("4") self.shortcutComboBox2.addItem("5") self.shortcutComboBox2.addItem("6") self.shortcutComboBox2.addItem("7") self.shortcutComboBox2.addItem("8") self.shortcutComboBox2.addItem("9") self.shortcutComboBox2.addItem("0") self.register_shortcut_listener() def register_shortcut_listener(self): self.engine.event_queue.remove_all() if self.iniSettings.value("shortcut_modifier") == "NONE": self.engine.event_queue.register_event([[self.shortcutKey], self.icon_activated, QSystemTrayIcon.Trigger]) elif self.iniSettings.value("shortcut_modifier") == "CTRL": self.engine.event_queue.register_event([['Lcontrol', self.shortcutKey], self.icon_activated, QSystemTrayIcon.Trigger]) self.engine.event_queue.register_event([['Rcontrol', self.shortcutKey], self.icon_activated, QSystemTrayIcon.Trigger]) elif self.iniSettings.value("shortcut_modifier") == "ALT": self.engine.event_queue.register_event([['LMenu', self.shortcutKey], self.icon_activated, QSystemTrayIcon.Trigger]) self.engine.event_queue.register_event([['RMenu', self.shortcutKey], self.icon_activated, QSystemTrayIcon.Trigger]) return True
class MatPlotLibBase(QWidget): def __init__(self, parent, file_dialog_service, h_margin=(0.8, 0.1), v_margin=(0.5, 0.15), h_axes=[Size.Scaled(1.0)], v_axes=[Size.Scaled(1.0)], nx_default=1, ny_default=1): QWidget.__init__(self, parent) self._file_dialog_service = file_dialog_service self._figure = Figure() self._canvas = FigureCanvas(self._figure) h = [Size.Fixed(h_margin[0]), *h_axes, Size.Fixed(h_margin[1])] v = [Size.Fixed(v_margin[0]), *v_axes, Size.Fixed(v_margin[1])] self._divider = Divider(self._figure, (0.0, 0.0, 1.0, 1.0), h, v, aspect=False) self._axes = LocatableAxes(self._figure, self._divider.get_position()) self._axes.set_axes_locator( self._divider.new_locator(nx=nx_default, ny=ny_default)) self._axes.set_zorder(2) self._axes.patch.set_visible(False) for spine in ['top', 'right']: self._axes.spines[spine].set_visible(False) self._figure.add_axes(self._axes) self._canvas.setParent(self) self._layout = QVBoxLayout(self) self._layout.setContentsMargins(0, 0, 0, 0) self._layout.addWidget(self._canvas) self.setLayout(self._layout) self._figure.canvas.mpl_connect('scroll_event', self._on_scroll) self._xy_extents = None self._background_cache = None self._decoration_artists = [] self._is_panning = False self._zoom_selector = _RectangleSelector(self._axes, self._zoom_selected) self._zoom_selector.set_active(False) self._x_extent_padding = 0.01 self._y_extent_padding = 0.01 self._axes.ticklabel_format(style='sci', axis='x', scilimits=(-4, 4)) self._axes.ticklabel_format(style='sci', axis='y', scilimits=(-4, 4)) self._active_tools = {} self._span = _SpanSeletor(self._axes, self._handle_span_select, 'horizontal', rectprops=dict(alpha=0.2, facecolor='red', edgecolor='k'), span_stays=True) self._span.set_on_select_none(self._handle_span_select_none) self.span = self._previous_span = None self._span_center_mouse_event = None self._span_left_mouse_event = None self._span_right_mouse_event = None self._figure.canvas.mpl_connect('button_press_event', self._handle_press) self._figure.canvas.mpl_connect('motion_notify_event', self._handle_move) self._figure.canvas.mpl_connect('button_release_event', self._handle_release) self._figure.canvas.mpl_connect('resize_event', self._handle_resize) self.activateTool(ToolType.span, self.isActiveDefault(ToolType.span)) self._pan_event = None self._pending_draw = None self._pending_artists_draw = None self._other_draw_events = [] self._draw_timer = QTimer(self) self._draw_timer.timeout.connect(self._do_draw_events) self._draw_timer.start(20) self._zoom_skew = None self._menu = QMenu(self) self._copy_image_action = QAction(self.tr('Copy To Clipboard'), self) self._copy_image_action.triggered.connect(self.copyToClipboard) self._copy_image_action.setShortcuts(QKeySequence.Copy) self._save_image_action = QAction(self.tr('Save As Image'), self) self._save_image_action.triggered.connect(self.saveAsImage) self._show_table_action = QAction(self.tr('Show Table'), self) self._show_table_action.triggered.connect(self.showTable) self._menu.addAction(self._copy_image_action) self._menu.addAction(self._save_image_action) self._menu.addAction(self._show_table_action) self.addAction(self._copy_image_action) self._table_view = None self._single_axis_zoom_enabled = True self._cached_label_width_height = None if hasattr(type(self), 'dataChanged'): self.dataChanged.connect(self._on_data_changed) self._options_view = None self._secondary_axes = self._secondary_y_extent = self._secondary_x_extent = None self._legend = None self._draggable_legend = None self._setting_axis_limits = False self.hasHiddenSeries = False enabledToolsChanged = pyqtSignal() spanChanged = pyqtSignal(SpanModel) hasHiddenSeriesChanged = pyqtSignal(bool) span = AutoProperty(SpanModel) hasHiddenSeries = AutoProperty(bool) def setOptionsView(self, options_view): self._options_view = options_view self._options_view.setSecondaryYLimitsEnabled( self._secondary_y_enabled()) self._options_view.setSecondaryXLimitsEnabled( self._secondary_x_enabled()) self._options_view.showGridLinesChanged.connect( self._update_grid_lines) self._options_view.xAxisLowerLimitChanged.connect( self._handle_options_view_limit_changed(x_min_changed=True)) self._options_view.xAxisUpperLimitChanged.connect( self._handle_options_view_limit_changed(x_max_changed=True)) self._options_view.yAxisLowerLimitChanged.connect( self._handle_options_view_limit_changed(y_min_changed=True)) self._options_view.yAxisUpperLimitChanged.connect( self._handle_options_view_limit_changed(y_max_changed=True)) self._options_view.xAxisLimitsChanged.connect( self._handle_options_view_limit_changed(x_min_changed=True, x_max_changed=True)) self._options_view.yAxisLimitsChanged.connect( self._handle_options_view_limit_changed(y_min_changed=True, y_max_changed=True)) self._options_view.secondaryXAxisLowerLimitChanged.connect( self._handle_options_view_secondary_limit_changed( x_min_changed=True)) self._options_view.secondaryXAxisUpperLimitChanged.connect( self._handle_options_view_secondary_limit_changed( x_max_changed=True)) self._options_view.secondaryYAxisLowerLimitChanged.connect( self._handle_options_view_secondary_limit_changed( y_min_changed=True)) self._options_view.secondaryYAxisUpperLimitChanged.connect( self._handle_options_view_secondary_limit_changed( y_max_changed=True)) self._options_view.secondaryXAxisLimitsChanged.connect( self._handle_options_view_secondary_limit_changed( x_min_changed=True, x_max_changed=True)) self._options_view.secondaryYAxisLimitsChanged.connect( self._handle_options_view_secondary_limit_changed( y_min_changed=True, y_max_changed=True)) def setLegendControl(self, legend_control): self._legend_control = legend_control self._legend_control.seriesUpdated.connect(self._legend_series_updated) self._legend_control.showLegendChanged.connect(self._show_legend) self._legend_control.seriesNameChanged.connect( self._handle_series_name_changed) self._legend_control.showSeriesChanged.connect( self._handle_show_series_changed) bind(self._legend_control, self, 'hasHiddenSeries', two_way=False) def _legend_series_updated(self): if self._legend is not None: self._show_legend(self._legend_control.showLegend) def _show_legend(self, show): if self._legend and not show: self._legend.remove() self._legend = None self.draw() elif show: if self._legend: self._legend.remove() show_series = self._legend_control.showSeries handles = [ h for h, s in zip(self._legend_control.seriesHandles, show_series) if s ] names = [ n for n, s in zip(self._legend_control.seriesNames, show_series) if s ] axes = (self._secondary_axes if self._secondary_axes and self._secondary_axes.get_visible() and self._secondary_axes.get_zorder() > self._axes.get_zorder() else self._axes) self._legend = self._create_legend( axes, handles, names, markerscale=self._get_legend_markerscale()) if self._get_legend_text_color() is not None: for text in self._legend.texts: text.set_color(self._get_legend_text_color()) self._draggable_legend = DraggableLegend(self._legend) self.draw() def _get_legend_markerscale(self): return 5 def _create_legend(self, axes, handles, names, **kwargs): return axes.legend(handles, names, **kwargs) def _get_legend_text_color(self): return None def _handle_series_name_changed(self, index, series_name): if self._legend is not None and index < len( self._legend_control.seriesHandles): visible_handles = [ h for h, s in zip(self._legend_control.seriesHandles, self._legend_control.showSeries) if s and h is not None ] try: legend_index = visible_handles.index( self._legend_control.seriesHandles[index]) except ValueError: return if legend_index < len(self._legend.texts): self._legend.texts[legend_index].set_text(series_name) self.draw() def _handle_show_series_changed(self, index, show_series): if index < len(self._legend_control.seriesHandles): self._set_series_visibility( self._legend_control.seriesHandles[index], show_series) if self._legend is not None: self._show_legend(self._legend_control.showLegend) else: self.draw() def _set_series_visibility(self, handle, visible): if not handle: return if hasattr(handle, 'set_visible'): handle.set_visible(visible) elif hasattr(handle, 'get_children'): for child in handle.get_children(): self._set_series_visibility(child, visible) def _update_grid_lines(self): show_grid_lines = False if self._options_view is None else self._options_view.showGridLines gridline_color = self._axes.spines['bottom'].get_edgecolor() gridline_color = gridline_color[0], gridline_color[1], gridline_color[ 2], 0.5 kwargs = dict(color=gridline_color, alpha=0.5) if show_grid_lines else {} self._axes.grid(show_grid_lines, **kwargs) self.draw() def _handle_options_view_limit_changed(self, x_min_changed=False, x_max_changed=False, y_min_changed=False, y_max_changed=False): def _(): if self._options_view is None or self._setting_axis_limits: return (x_min, x_max), (y_min, y_max) = (new_x_min, new_x_max), ( new_y_min, new_y_max) = self._get_xy_extents() (x_opt_min, x_opt_max), (y_opt_min, y_opt_max) = self._get_options_view_xy_extents() if x_min_changed: new_x_min = x_opt_min if x_max_changed: new_x_max = x_opt_max if y_min_changed: new_y_min = y_opt_min if y_max_changed: new_y_max = y_opt_max if [new_x_min, new_x_max, new_y_min, new_y_max ] != [x_min, x_max, y_min, y_max]: self._xy_extents = (new_x_min, new_x_max), (new_y_min, new_y_max) self._set_axes_limits() self.draw() return _ def _get_options_view_xy_extents(self): (x_data_min, x_data_max), (y_data_min, y_data_max) = self._get_data_xy_extents() x_min = x_data_min if np.isnan( self._options_view.xAxisLowerLimit ) else self._options_view.xAxisLowerLimit x_max = x_data_max if np.isnan( self._options_view.xAxisUpperLimit ) else self._options_view.xAxisUpperLimit y_min = y_data_min if np.isnan( self._options_view.yAxisLowerLimit ) else self._options_view.yAxisLowerLimit y_max = y_data_max if np.isnan( self._options_view.yAxisUpperLimit ) else self._options_view.yAxisUpperLimit return (x_min, x_max), (y_min, y_max) def _handle_options_view_secondary_limit_changed(self, x_min_changed=False, x_max_changed=False, y_min_changed=False, y_max_changed=False): def _(): if self._options_view is None or self._setting_axis_limits: return updated = False (x_opt_min, x_opt_max), ( y_opt_min, y_opt_max) = self._get_options_view_secondary_xy_extents() if self._has_secondary_y_extent() and (y_min_changed or y_max_changed): y_min, y_max = new_y_min, new_y_max = self._get_secondary_y_extent( ) if y_min_changed: new_y_min = y_opt_min if y_max_changed: new_y_max = y_opt_max if [new_y_min, new_y_max] != [y_min, y_max]: self._secondary_y_extent = (new_y_min, new_y_max) updated = True if self._has_secondary_x_extent() and (x_min_changed or x_max_changed): x_min, x_max = new_x_min, new_x_max = self._get_secondary_x_extent( ) if x_min_changed: new_x_min = x_opt_min if x_max_changed: new_x_max = x_opt_max if [new_x_min, new_x_max] != [x_min, x_max]: self._secondary_x_extent = (new_x_min, new_x_max) updated = True if updated: self._set_axes_limits() self.draw() return _ def _get_options_view_secondary_xy_extents(self): x_data_min, x_data_max = self._get_data_secondary_x_extent() y_data_min, y_data_max = self._get_data_secondary_y_extent() x_min = x_data_min if np.isnan( self._options_view.secondaryXAxisLowerLimit ) else self._options_view.secondaryXAxisLowerLimit x_max = x_data_max if np.isnan( self._options_view.secondaryXAxisUpperLimit ) else self._options_view.secondaryXAxisUpperLimit y_min = y_data_min if np.isnan( self._options_view.secondaryYAxisLowerLimit ) else self._options_view.secondaryYAxisLowerLimit y_max = y_data_max if np.isnan( self._options_view.secondaryYAxisUpperLimit ) else self._options_view.secondaryYAxisUpperLimit return (x_min, x_max), (y_min, y_max) def _on_data_changed(self): self._cached_label_width_height = None def closeEvent(self, event): QWidget.closeEvent(self, event) if event.isAccepted(): self._zoom_selector.onselect = self._span.onselect = self._span._select_none_handler = None def set_divider_h_margin(self, h_margin): h = [ Size.Fixed(h_margin[0]), Size.Scaled(1.0), Size.Fixed(h_margin[1]) ] self._divider.set_horizontal(h) def set_divider_v_margin(self, v_margin): v = [ Size.Fixed(v_margin[0]), Size.Scaled(1.0), Size.Fixed(v_margin[1]) ] self._divider.set_vertical(v) @property def x_extent_padding(self): return self._x_extent_padding @x_extent_padding.setter def x_extent_padding(self, value): self._x_extent_padding = value @property def y_extent_padding(self): return self._y_extent_padding @y_extent_padding.setter def y_extent_padding(self, value): self._y_extent_padding = value def _in_interval(self, value, interval): return interval[0] <= value <= interval[1] def _interval_skew(self, value, interval): return (value - interval[0]) / (interval[1] - interval[0]) def _in_x_scroll_zone(self, event): return self._in_interval(event.x, self._axes.bbox.intervalx ) and event.y <= self._axes.bbox.intervaly[1] def _in_y_scroll_zone(self, event): return self._in_interval(event.y, self._axes.bbox.intervaly ) and event.x <= self._axes.bbox.intervalx[1] def _on_scroll(self, event): if self._secondary_axes is not None: self._handle_scroll_secondary(event) in_x = self._in_x_scroll_zone(event) in_y = self._in_y_scroll_zone(event) if in_x or in_y and event.button in ['up', 'down']: (x_min, x_max), (y_min, y_max) = self._get_actual_xy_extents() if (in_x and self._single_axis_zoom_enabled) or (in_x and in_y): skew = self._zoom_skew and self._zoom_skew[0] skew = self._interval_skew( event.x, self._axes.bbox.intervalx) if skew is None else skew x_min, x_max = self._zoom(x_min, x_max, skew, event.button) if (in_y and self._single_axis_zoom_enabled) or (in_x and in_y): skew = self._zoom_skew and self._zoom_skew[1] skew = self._interval_skew( event.y, self._axes.bbox.intervaly) if skew is None else skew y_min, y_max = self._zoom(y_min, y_max, skew, event.button) self._xy_extents = (x_min, x_max), (y_min, y_max) self._set_axes_limits() self.draw() def _in_secondary_y_scroll_zone(self, event): return self._in_interval(event.y, self._axes.bbox.intervaly) and \ event.x >= self._axes.bbox.intervalx[1] def _in_secondary_x_scroll_zone(self, event): return self._in_interval(event.x, self._axes.bbox.intervalx) and \ event.y >= self._axes.bbox.intervaly[1] def _handle_scroll_secondary(self, event): if self._has_secondary_y_extent(): in_secondary_y = self._in_secondary_y_scroll_zone(event) if in_secondary_y and event.button in ['up', 'down']: self._secondary_y_extent = self._zoom( *self._get_secondary_y_extent(), self._interval_skew(event.y, self._axes.bbox.intervaly), event.button) if self._has_secondary_x_extent(): in_secondary_x = self._in_secondary_x_scroll_zone(event) if in_secondary_x and event.button in ['up', 'down']: self._secondary_x_extent = self._zoom( *self._get_secondary_x_extent(), self._interval_skew(event.x, self._axes.bbox.intervalx), event.button) def _get_zoom_multiplier(self): return 20 / 19 def _zoom(self, min_, max_, skew, direction): zoom_multiplier = self._get_zoom_multiplier( ) if direction == 'up' else 1 / self._get_zoom_multiplier() range_ = max_ - min_ diff = (range_ * (1 / zoom_multiplier)) - range_ max_ += diff * (1 - skew) min_ -= diff * skew return min_, max_ def _set_axes_limits(self): try: self._setting_axis_limits = True if self._secondary_axes is not None: self._set_secondary_axes_limits() self._update_ticks() (x_min, x_max), (y_min, y_max) = self._get_xy_extents() if self._options_view is not None: if self._options_view.x_limits: self._options_view.setXLimits(float(x_min), float(x_max)) if self._options_view.y_limits: self._options_view.setYLimits(float(y_min), float(y_max)) self._axes.set_xlim(*_safe_limits(x_min, x_max)) self._axes.set_ylim(*_safe_limits(y_min, y_max)) finally: self._setting_axis_limits = False def _set_secondary_axes_limits(self): if self._options_view is not None: if self._options_view.secondary_y_limits: enabled = self._secondary_y_enabled() secondary_y_min, secondary_y_max = self._get_secondary_y_extent( ) if enabled else (float('nan'), float('nan')) self._options_view.setSecondaryYLimitsEnabled(enabled) self._options_view.setSecondaryYLimits(float(secondary_y_min), float(secondary_y_max)) if self._options_view.secondary_x_limits: enabled = self._secondary_x_enabled() secondary_x_min, secondary_x_max = self._get_secondary_x_extent( ) if enabled else (float('nan'), float('nan')) self._options_view.setSecondaryXLimitsEnabled(enabled) self._options_view.setSecondaryXLimits(float(secondary_x_min), float(secondary_x_max)) if self._has_secondary_y_extent(): self._secondary_axes.set_ylim(*_safe_limits( *self._get_secondary_y_extent())) if self._has_secondary_x_extent(): self._secondary_axes.set_xlim(*_safe_limits( *self._get_secondary_x_extent())) def _secondary_y_enabled(self): return True if self._secondary_axes and self._secondary_axes.get_visible( ) and self._has_secondary_y_extent() else False def _secondary_x_enabled(self): return True if self._secondary_axes and self._secondary_axes.get_visible( ) and self._has_secondary_x_extent() else False def _set_axes_labels(self): self._axes.set_xlabel(self.data.xAxisTitle) self._axes.set_ylabel(self.data.yAxisTitle) def _set_center(self, center): if not all(c is not None for c in center): center = (0, 0) x_extent, y_extent = self._get_xy_extents() span = x_extent[1] - x_extent[0], y_extent[1] - y_extent[0] x_extent = center[0] - span[0] / 2, center[0] + span[0] / 2 y_extent = center[1] - span[1] / 2, center[1] + span[1] / 2 self._xy_extents = x_extent, y_extent def _get_xy_extents(self): if self.data is None: return (0, 0), (0, 0) if self._xy_extents is None: return self._get_data_xy_extents() return self._xy_extents def _get_data_xy_extents(self): if self.data is None: return (0, 0), (0, 0) (x_min, x_max), (y_min, y_max) = self.data.get_xy_extents() return self._pad_extent(x_min, x_max, self.x_extent_padding), self._pad_extent( y_min, y_max, self.y_extent_padding) def _has_secondary_y_extent(self): return hasattr(self.data, 'get_secondary_y_extent') def _get_secondary_y_extent(self): if self._secondary_y_extent is not None: return self._secondary_y_extent if self.data is not None: return self._get_data_secondary_y_extent() return (0, 0) def _get_data_secondary_y_extent(self): if self.data is None: return (0, 0) return self._pad_extent(*self.data.get_secondary_y_extent(), self.y_extent_padding) def _has_secondary_x_extent(self): return hasattr(self.data, 'get_secondary_x_extent') def _get_secondary_x_extent(self): if self._secondary_x_extent is not None: return self._secondary_x_extent if self.data is not None: return self._get_data_secondary_x_extent() return (0, 0) def _get_data_secondary_x_extent(self): if self.data is None or not hasattr(self.data, 'get_secondary_x_extent'): return (0, 0) return self._pad_extent(*self.data.get_secondary_x_extent(), self.x_extent_padding) def _get_actual_xy_extents(self): return self._axes.get_xlim(), self._axes.get_ylim() def _pad_extent(self, min_, max_, padding): min_, max_ = self._zero_if_nan(min_), self._zero_if_nan(max_) range_ = max_ - min_ return min_ - padding * range_, max_ + padding * range_ def _zoom_selected(self, start_pos, end_pos): x_min, x_max = min(start_pos.xdata, end_pos.xdata), max(start_pos.xdata, end_pos.xdata) y_min, y_max = min(start_pos.ydata, end_pos.ydata), max(start_pos.ydata, end_pos.ydata) self._xy_extents = (x_min, x_max), (y_min, y_max) self._set_axes_limits() self.draw() def _handle_span_select(self, x_min, x_max): x_min, x_max = self._round_to_bin_width(x_min, x_max) self._update_span_rect(x_min, x_max) self.span = SpanModel(self, x_min, x_max) self.draw() def _handle_span_select_none(self): self.span = None def _handle_press(self, event): if event.button == 1: if self._is_panning: self._pan_event = event elif self._span.active: self._handle_span_press(event) def _handle_move(self, event): if event.xdata and self._pan_event: self._handle_pan_move(event) elif event.xdata and any(self._span_events()): self._handle_span_move(event) def _handle_release(self, event): if self._pan_event: self._pan_event = None elif any(self._span_events()): self._handle_span_release(event) def _handle_pan_move(self, event): from_x, from_y = self._axes.transData.inverted().transform( (self._pan_event.x, self._pan_event.y)) to_x, to_y = self._axes.transData.inverted().transform( (event.x, event.y)) self._pan(from_x - to_x, from_y - to_y) self._pan_event = event def _pan(self, delta_x, delta_y): (x_min, x_max), (y_min, y_max) = self._get_xy_extents() self._xy_extents = (x_min + delta_x, x_max + delta_x), (y_min + delta_y, y_max + delta_y) self._set_axes_limits() self.draw() def _span_events(self): return self._span_center_mouse_event, self._span_left_mouse_event, self._span_right_mouse_event def _handle_span_press(self, event): if not event.xdata: return span_min, span_max = (self.span.left, self.span.right) if self.span else (0, 0) edge_tolerance = self._span_tolerance() if abs(span_min - event.xdata) < edge_tolerance: self._span.active = False self._span_left_mouse_event = event elif abs(span_max - event.xdata) < edge_tolerance: self._span.active = False self._span_right_mouse_event = event elif span_min < event.xdata < span_max: self._span.active = False self._span_center_mouse_event = event def _handle_span_move(self, event): if not self.span: return x_min, x_max = self.span.left, self.span.right last_event = next(x for x in self._span_events() if x) diff_x = event.xdata - last_event.xdata if self._span_center_mouse_event is not None: self._update_span_rect(x_min + diff_x) elif self._span_left_mouse_event is not None: self._update_span_rect(x_min + diff_x, x_max) elif self._span_right_mouse_event is not None: self._update_span_rect(x_min, x_max + diff_x) self.draw([self._span.rect]) def _handle_span_release(self, _event): x_min = self._span.rect.get_x() x_max = x_min + self._span.rect.get_width() x_min, x_max = self._round_to_bin_width(x_min, x_max) self._update_span_rect(x_min, x_max) self.span = SpanModel(self, x_min, x_max) self.draw() self._span.active = True self._span_center_mouse_event = self._span_left_mouse_event = self._span_right_mouse_event = None def _update_span_rect(self, x_min, x_max=None): self._span.rect.set_x(x_min) self._span.stay_rect.set_x(x_min) if x_max: self._span.rect.set_width(x_max - x_min) self._span.stay_rect.set_width(x_max - x_min) def _round_to_bin_width(self, x_min, x_max): return x_min, x_max def _span_tolerance(self): return 5 def toolEnabled(self, _tool_type): return False def toolAvailable(self, _tool_type): return False def activateTool(self, tool_type, active): if tool_type == ToolType.zoom: self._zoom_selector.set_active(active) elif tool_type == ToolType.span: if self._span.active and not active: self._previous_span = self.span self.span = None for r in [self._span.rect, self._span.stay_rect]: self._remove_artist(r) elif not self._span.active and active: self.span = self._previous_span for r in [self._span.rect, self._span.stay_rect]: self._add_artist(r) self._span.active = active self.draw() elif tool_type == ToolType.pan: self._is_panning = active self._active_tools[tool_type] = active def toolActive(self, tool_type): return self._active_tools.get(tool_type, False) def isActiveDefault(self, _tool_type): return False def _add_artist(self, artist): self._axes.add_artist(artist) self._decoration_artists.append(artist) def _remove_artist(self, artist): artist.remove() if artist in self._decoration_artists: self._decoration_artists.remove(artist) def _handle_resize(self, _event): self._update_ticks() return self.draw() def draw(self, artists=None): if artists is None: def _update(): for a in self._decoration_artists: a.remove() self._canvas.draw() self._background_cache = self._canvas.copy_from_bbox( self._figure.bbox) for a in self._decoration_artists: self._axes.add_artist(a) self._axes.draw_artist(a) self._canvas.update() self._pending_draw = _update else: def _update(): if self._background_cache is None: raise RuntimeError('Must run draw before drawing artists!') self._canvas.restore_region(self._background_cache) for a in artists: self._axes.draw_artist(a) self._canvas.update() self._pending_artists_draw = _update def _do_draw_events(self): if self._pending_draw is not None: self._pending_draw() self._pending_draw = None if self._pending_artists_draw is not None: self._pending_artists_draw() self._pending_artists_draw = None if self._other_draw_events: for draw_event in self._other_draw_events: draw_event() self._other_draw_events = [] def addDrawEvent(self, draw_event): self._other_draw_events.append(draw_event) def resetZoom(self): self._secondary_y_extent = self._secondary_x_extent = None self._xy_extents = None self._set_axes_limits() self.draw() def _twinx(self, ylabel): axes = self._axes.twinx() for spine in ['top', 'left']: axes.spines[spine].set_visible(False) axes.set_ylabel(ylabel) axes.set_zorder(1) return axes @property def axes(self): return self._axes @property def secondary_axes(self): if self._secondary_axes is None: self._set_secondary_axes(self._twinx('')) return self._secondary_axes def _set_secondary_axes(self, axes): self._secondary_axes = axes @staticmethod def sizeHint(): """function::sizeHint() Override the default sizeHint to ensure the plot has an initial size """ return QSize(600, 400) def minimumSizeHint(self): """function::sizeHint() Override the default sizeHint to ensure the plot does not shrink below minimum size """ return self.sizeHint() @staticmethod def _zero_if_nan(value): return value if not isinstance(value, float) or not np.isnan(value) else 0 def canShowTable(self): return hasattr(self, 'data') and self.data is not None and hasattr( self.data, 'table') def contextMenuEvent(self, event): self._show_table_action.setEnabled(self.canShowTable()) self._menu.exec_(event.globalPos()) def copyToClipboard(self): with BytesIO() as buffer: self._figure.savefig(buffer, facecolor=self._figure.get_facecolor()) QApplication.clipboard().setImage( QImage.fromData(buffer.getvalue())) def saveAsImage(self): filename = self._file_dialog_service.get_save_filename( self, self.tr('Portable Network Graphics (*.png)')) if filename: self._figure.savefig(filename, facecolor=self._figure.get_facecolor()) def showTable(self): if self.canShowTable(): self._table_view = TableView(None) self._table_view.pasteEnabled = False self._table_view.setModel(self.data.table) self._table_view.setMinimumSize(800, 600) self._table_view.show() def _update_ticks(self): if not self.data: return if hasattr(self.data, 'x_labels'): step = self.data.x_tick_interval if hasattr( self.data, 'x_tick_interval') else None x_ticks, x_labels = self._get_labels(self.data.x_labels, step, horizontal=True) self._axes.set_xticks(x_ticks) self._axes.set_xticklabels(x_labels) if hasattr(self.data, 'y_labels'): step = self.data.y_tick_interval if hasattr( self.data, 'y_tick_interval') else None y_ticks, y_labels = self._get_labels(self.data.y_labels, step, horizontal=False) self._axes.set_yticks(y_ticks) self._axes.set_yticklabels(y_labels) def _get_labels(self, labels, step, horizontal=True): (x0, x1), (y0, y1) = self._get_xy_extents() start, end = (int(x0), int(x1)) if horizontal else (int(y0), int(y1)) visible_points = end - start if not (step and step > 0): width, height = self._get_label_width_height(labels) axes_bbox = self._axes.get_window_extent( self._figure.canvas.get_renderer()).transformed( self._figure.dpi_scale_trans.inverted()) plot_size = (axes_bbox.width if horizontal else axes_bbox.height) * self._figure.dpi size = (width if horizontal else height) if plot_size == 0 or size == 0: n_labels = 16 else: n_labels = int(plot_size / size) if n_labels == 0: n_labels = 16 step = int(visible_points / n_labels) + 1 else: step = int(step) indexes = list(range(len(labels))) display_labels = list(labels) for i in indexes: if i % step: display_labels[i] = '' return indexes, display_labels def _get_label_width_height(self, labels): if not self._cached_label_width_height: font = MatPlotLibFont.default() width = 0 height = 0 for label in labels: next_width, next_height = font.get_size( str(label), matplotlib.rcParams['font.size'], self._figure.dpi) width = max(width, next_width) height = max(height, next_height) self._cached_label_width_height = width, height return self._cached_label_width_height def _create_new_axes(self, nx=1, ny=1) -> LocatableAxes: axes = LocatableAxes(self._figure, self._divider.get_position()) axes.set_axes_locator(self._divider.new_locator(nx=nx, ny=ny)) self._figure.add_axes(axes) return axes @staticmethod def _create_secondary_xy_axes(figure, divider, nx=1, ny=1, visible=False, z_order=1): axes = LocatableAxes(figure, divider.get_position()) axes.set_axes_locator(divider.new_locator(nx=nx, ny=ny)) axes.xaxis.tick_top() axes.xaxis.set_label_position('top') axes.yaxis.tick_right() axes.yaxis.set_label_position('right') axes.patch.set_visible(visible) axes.set_zorder(z_order) figure.add_axes(axes) axes.ticklabel_format(style='sci', axis='x', scilimits=(-4, 4)) axes.ticklabel_format(style='sci', axis='y', scilimits=(-4, 4)) return axes @staticmethod def _create_shared_axes(figure, divider, shared_axes, nx=1, ny=1, visible=False, z_order=1): axes = LocatableAxes(figure, divider.get_position(), sharex=shared_axes, sharey=shared_axes, frameon=False) axes.set_axes_locator(divider.new_locator(nx=nx, ny=ny)) for spine in axes.spines.values(): spine.set_visible(False) for axis in axes.axis.values(): axis.set_visible(False) axes.patch.set_visible(False) axes.set_visible(False) axes.set_zorder(z_order) figure.add_axes(axes) return axes
class TableView(QTableView): def __init__(self, parent): QTableView.__init__(self, parent) self._menu = QMenu(self) self._copy_action = QAction(self.tr('Copy'), self) self._copy_action.triggered.connect(self.copy) self._copy_action.setShortcuts(QKeySequence.Copy) self._copy_with_headers_action = QAction(self.tr('Copy With Headers'), self) self._copy_with_headers_action.triggered.connect(self.copyWithHeaders) self._paste_action = QAction(self.tr('Paste'), self) self._paste_action.triggered.connect(self.paste) self._paste_action.setShortcuts(QKeySequence.Paste) self._menu.addAction(self._copy_action) self._menu.addAction(self._copy_with_headers_action) self._menu.addAction(self._paste_action) self.addAction(self._copy_action) self.addAction(self._copy_with_headers_action) self.addAction(self._paste_action) @property def pasteEnabled(self): return self._paste_action.isEnabled() @pasteEnabled.setter def pasteEnabled(self, value): self._paste_action.setEnabled(value) @property def copyEnabled(self): return self._copy_action.isEnabled() @copyEnabled.setter def copyEnabled(self, value): self._copy_action.setEnabled(value) def setModel(self, model): QTableView.setModel(self, model) if model: if model.parent() is None: model.setParent(self) model.dataChanged.connect(self._handle_data_changed) self._update_cell_spans() def copy(self): self._do_copy() def copyWithHeaders(self): self._do_copy(include_headers=True) def _do_copy(self, include_headers=False): indexes = self._get_selected_indexes() current_row = None paste_data = '' columns = [] columns_added = [] row_header_added = False for i in indexes: if include_headers and i.column() not in columns_added: columns.append(self.model().headerData(i.column(), Qt.Horizontal) or '') columns_added.append(i.column()) if current_row is not None: if i.row() != current_row: paste_data += '\n' row_header_added = False else: paste_data += '\t' if include_headers and not row_header_added: paste_data += (self.model().headerData(i.row(), Qt.Vertical) or '') + '\t' row_header_added = True data = self.model().data(i) paste_data += str(data) if data is not None else '' current_row = i.row() if include_headers: header_row = '\t' + '\t'.join(columns) + '\n' paste_data = header_row + paste_data QApplication.clipboard().setText(paste_data) def paste(self): value = QApplication.clipboard().text() rows = value.strip('\n').split('\n') selected_indexes = self._get_selected_indexes() if selected_indexes: start_row = selected_indexes[0].row() start_column = selected_indexes[0].column() else: start_row = start_column = 0 indexes = [] if hasattr(self.model(), 'beginPaste'): self.model().beginPaste() for row_id, row_text in zip(range(len(rows)), rows): cells = row_text.split('\t') for col_id, cell_text in zip(range(len(cells)), cells): index = self.model().createIndex(row_id + start_row, col_id + start_column) self.model().setData(index, cell_text) indexes.append(index) if hasattr(self.model(), 'endPaste'): self.model().endPaste(indexes[0], indexes[-1]) def _get_selected_indexes(self): return list(sorted(sorted(self.selectedIndexes(), key=lambda i: i.column()), key=lambda i: i.row())) def contextMenuEvent(self, event): self._menu.exec_(event.globalPos()) def _handle_data_changed(self, start_index, end_index): self._update_cell_spans(start_index, end_index) def _update_cell_spans(self, start_index=QModelIndex(), end_index=QModelIndex()): if isinstance(self.model(), QSortFilterProxyModel): return start_index = start_index if start_index.isValid() else self.model().createIndex(0, 0) end_index = end_index if end_index.isValid() else self.model().createIndex(self.model().rowCount(), self.model().columnCount()) for row in range(start_index.row(), end_index.row()): for column in range(start_index.column(), end_index.column()): current_index = self.model().createIndex(row, column) row_span = self.model().data(current_index, RowSpanRole) col_span = self.model().data(current_index, ColumnSpanRole) if (isinstance(row_span, int) and row_span != self.rowSpan(row, column)) or (isinstance(col_span, int) and col_span != self.columnSpan(row, column)): row_span = row_span if isinstance(row_span, int) else 1 col_span = col_span if isinstance(col_span, int) else 1 self.setSpan(row, column, row_span, col_span)
class DyTableWidget(QTableWidget): highlightBackground = QColor('#FFD700') def __init__(self, parent=None, readOnly=False, index=True, floatCut=True, autoScroll=True, floatRound=2): """ @index: 是否要插入默认行索引(Org.) @floatRound: 小数点后格式化成几位, 只在@floatCut is True时有效 """ super().__init__(parent) self.setSortingEnabled(True) self.verticalHeader().setVisible(False) if readOnly: self.setEditTriggers(QTableWidget.NoEditTriggers) self.setSelectionBehavior(QAbstractItemView.SelectRows) self._role = Qt.DisplayRole if readOnly else Qt.EditRole self.__index = index # 原始插入的行索引 self._floatCut = floatCut self._floatRoundFormat = '%.{0}f'.format(floatRound) self._autoForegroundCol = None # 指定前景色关键列,如果此列对应的值改变时,将改变所对应行的前景色。包含'Org.'列 self._enableAutoScroll = autoScroll self.setColNames([]) self._initItemMenu() self._initRows() def _initRows(self): self._itemsMap = {} # 由行关键字建立的item的字典, {key: [item]} # mark self._markedItem = None self._markedItemOriginalBackground = None self._markedItemsOriginalForeground = [] # highlight, item is one of item in one row self._highlightedItems = [ ] # [[item, [highlightedItemsOriginalForeground], [highlightedItemsOriginalBackground]]] # find self._findItems = [] self._curFindItemPos = 0 def _clearHighlight(self): # reproduce highlighed items because during cancel highlight procedure element will be deleted from @self._highlightedItems highlightedItems = [item[0] for item in self._highlightedItems] for item in highlightedItems: self._highlight(item) def _clearVisualEffects(self): self._mark(self._markedItem) self._clearHighlight() def __getitem__(self, indices): row, col = indices item = self._getItem(row, col) if item is None: return None return DyCommon.toNumber(item.data(self._role)) def _getItem(self, row, col): # row if isinstance(row, str): item = None if row in self._itemsMap: item = self._itemsMap[row][0] if item is None: return None row = item.row() # column if isinstance(col, int): if self.__index: col += 1 else: col = self._getColPos(col) # get item try: item = self.item(row, col) except: item = None return item def _updateItemsMap(self, rowKey, col, item): if rowKey not in self._itemsMap: self._itemsMap[rowKey] = [] rowLen = len(self._itemsMap[rowKey]) for i in range(rowLen, col + 1): self._itemsMap[rowKey].append(None) self._itemsMap[rowKey][col] = item def _getColPos(self, colName): for i in range(self.columnCount()): item = self.horizontalHeaderItem(i) if colName == item.text(): return i return None def _updateItem(self, row, col, value): """ Update one item, @row and @col can be string or integer. It's gengeral function. @col is included Org. for @col is integer, i.e. it's absolute updating. """ if isinstance(row, str): self._updateItemByRowKey(row, col, value) else: self._updateItemByRowPos(row, col, value) def _updateItemByRowPos(self, row, col, value): if isinstance(col, str): colPos = self._getColPos(col) if colPos is None: # insert a new column with column name colPos = self.columnCount() item = QTableWidgetItem(col) self.setHorizontalHeaderItem(colPos, item) col = colPos # now we take it by positions self._updateItemByPos(row, col, value) def _getColPosWithCreation(self, colName): colPos = self._getColPos(col) if colPos is None: # insert a new column with column name colPos = self.columnCount() item = QTableWidgetItem(col) self.setHorizontalHeaderItem(colPos, item) return colPos def _setAutoRowForeground(self, item): if self._autoForegroundCol is None: return # ignore 'Org.' column if self.__index and item.column() == 0: return # get forground of reference item row = item.row() refItem = self.item(row, self._autoForegroundCol) if not refItem: return # set forground same as reference item item.setForeground(refItem.foreground()) # we still need to go through row if value of reference item changed if item.column() == self._autoForegroundCol: # get foreground for row color = self.getForegroundOverride(item.data(self._role)) if color is None: if item.background() == Qt.white: # for qdarkstyle color = Qt.black else: color = Qt.white # no foreground changed if item.foreground() == color: return for i in range(self.columnCount()): if self.__index and i == 0: continue item = self.item(row, i) if item: item.setForeground(color) def _setItemData(self, item, value): """ 设置Item的值和相应的格式 string值将会保持原始格式 """ assert value is None or isinstance(value, float) or isinstance( value, int) or isinstance(value, str), 'type(value) is {0}'.format( type(value)) # set data if isinstance(value, float): if not np.isnan(value): if self._floatCut: value = self._floatRoundFormat % value else: value = None item.setData(self._role, value) # set auto row color self._setAutoRowForeground(item) if self._enableAutoScroll: self.scrollToItem(item) def _setItemDataFast(self, item, value): """ 快速设置Item的值 string值将会保持原始格式 """ assert value is None or isinstance(value, float) or isinstance( value, int) or isinstance(value, str), 'type(value) is {0}'.format( type(value)) # set data if isinstance(value, float): if not np.isnan(value): if self._floatCut: value = self._floatRoundFormat % value else: value = None item.setData(self._role, value) def _newItemByRowKey(self, rowKey, col, value): if rowKey in self._itemsMap: row = self._itemsMap[rowKey][0].row() else: row = self.rowCount() if isinstance(col, str): col = self._getColPosWithCreation(col) # now we take it by positions item = self._updateItemByPos(row, col, value) # update to items map self._updateItemsMap(rowKey, col, item) def _updateItemByRowKey(self, rowKey, col, value): isExistingItem = False if rowKey in self._itemsMap: if isinstance(col, str): colPos = self._getColPos(col) else: colPos = col if colPos is not None: if colPos < len(self._itemsMap[rowKey]): if self._itemsMap[rowKey][ colPos] is not None: # item existing self._setItemData(self._itemsMap[rowKey][colPos], value) isExistingItem = True if not isExistingItem: self._newItemByRowKey(rowKey, col, value) def _updateItemByPos(self, row, col, value): # get item if existing item = self.item(row, col) # new item if item is None: # enlarge rowCount = self.rowCount() colCount = self.columnCount() if row >= rowCount: self.setRowCount(row + 1) if col >= colCount: self.setColumnCount(col + 1) # add new item item = DyTableWidgetItem(self._role) self.setItem(row, col, item) # Should call @setItem firstly, then set data self._setItemData(item, value) return item def _update(self, indices, value): """ Update one row by @indices is row key or row position, @value is [x, x, x, ...] or one item by @indices is (row key or row position, column name or column position), @value is x. position is from 0. """ if isinstance(indices, tuple): row, col = indices else: row, col = indices, None # add one row # update Org. if self.__index: if isinstance(row, str): # row key if row not in self._itemsMap: # first updating self._updateItem(row, 0, self.rowCount() + 1) else: if not self.item(row, 0): # first updating self._updateItem(row, 0, row + 1) # value is row No. from 1 offset = 1 # offset for column else: offset = 0 # offset for column if col is None: # row for col, v in enumerate(value, offset): self._updateItem(row, col, v) else: # one item self._updateItem(row, (col + offset) if isinstance(col, int) else col, value) def __setitem__(self, indices, value): """ add one row like obj[x] = [v,v,..] add one like obj[x,y] = v """ self.setSortingEnabled(False) self._update(indices, value) self.resizeColumnsToContents() self.resizeRowsToContents() self.setSortingEnabled(True) def addColNames(self, names): colStart = self.columnCount() self.setColumnCount(colStart + len(names)) for col, name in enumerate(names, colStart): colItem = self.horizontalHeaderItem(col) if colItem is None: colItem = QTableWidgetItem(col) self.setHorizontalHeaderItem(col, colItem) colItem.setText(name) #self.resizeColumnsToContents() def hasIndex(self): return self.__index def addColName(self, col, name): if self.__index: col += 1 if col >= self.columnCount(): self.setColumnCount(col + 1) colItem = self.horizontalHeaderItem(col) if colItem is None: colItem = QTableWidgetItem(col) self.setHorizontalHeaderItem(col, colItem) colItem.setText(name) #self.resizeColumnsToContents() def setHeaderForeground(self, color): """ 只能设置整个header http://stackoverflow.com/questions/36196988/color-individual-horizontal-headers-of-qtablewidget-in-pyqt @color: string, like 'red' """ self.horizontalHeader().setStyleSheet('color:' + color) def setColName(self, col, name): if self.__index: col += 1 colItem = self.horizontalHeaderItem(col) if colItem: colItem.setText(name) self.resizeColumnsToContents() def setColNames(self, names=None): """ @names:[name1, name2] """ if names is None: return if self.__index: newNames = ['Org.'] + names else: newNames = names self.setColumnCount(len(newNames)) self.setHorizontalHeaderLabels(newNames) self.resizeColumnsToContents() def setItemForeground(self, row, col, color): if self.__index: col += 1 try: self.item(row, col).setForeground(color) except Exception as ex: pass def setItemBackground(self, row, col, color): if self.__index: col += 1 try: self.item(row, col).setBackground(color) except Exception as ex: pass def setRowForeground(self, row, color): try: colCount = self.columnCount() start, end = (1, colCount) if self.__index else (0, colCount) for col in range(start, end): self.item(row, col).setForeground(color) except Exception as ex: pass def setRowBackground(self, row, color): try: colCount = self.columnCount() start, end = (1, colCount) if self.__index else (0, colCount) for col in range(start, end): self.item(row, col).setBackground(color) except Exception as ex: pass def append(self, rows, header=None, autoForegroundColName=None): """ @rows: [[x,x,x],[x,x,x],...] @header: [x,x,x] """ self.setSortingEnabled(False) if header: self.setColNames(header) if autoForegroundColName: self.setAutoForegroundCol(autoForegroundColName) rowCount = self.rowCount() self.setRowCount(rowCount + len(rows)) for rowIndex, rowData in enumerate(rows, rowCount): self._update(rowIndex, rowData) self.resizeColumnsToContents() self.resizeRowsToContents() self.setSortingEnabled(True) def appendRow(self, row, new=False, disableSorting=True): """ @row: [x,x,x] @return: row position of added row(starting from 0) """ if disableSorting: self.setSortingEnabled(False) if new: self.clearAllRows() rowCount = self.rowCount() self._update(rowCount, row) self.resizeColumnsToContents() self.resizeRowsToContents() if disableSorting: self.setSortingEnabled(True) return rowCount def setAutoForegroundCol(self, colName): self._autoForegroundCol = self._getColPos(colName) def getAutoForegroundColName(self): if self._autoForegroundCol is None: return None autoForegroundCol = self._autoForegroundCol - 1 if self.__index else self._autoForegroundCol return self.getColName(autoForegroundCol) def _autoScrollAct(self): self._enableAutoScroll = not self._enableAutoScroll if self._enableAutoScroll: self._autoScrollAction.setText('关闭自动滚动') else: self._autoScrollAction.setText('开启自动滚动') def _visibleMarkAct(self): if self._markedItem is not None: self.scrollToItem(self._markedItem) def _isInHighlight(self, item): row = item.row() for highlightedItems in self._highlightedItems: if highlightedItems[0].row() == row: return True return False def _setMark(self): row = self._markedItem.row() markBg = QColor(Qt.yellow) self.setRowBackground(row, markBg) for col in range(self.columnCount()): if self.__index and col == 0: self._markedItemsOriginalForeground.append(None) continue item = self.item(row, col) fg = item.foreground().color() # only change qdarkstyle default foreground if fg == QColor(0, 0, 0) or fg == QColor(192, 192, 192): item.setForeground(QColor(0, 0, 0)) # for qdarkstyle default foreground if fg == QColor(0, 0, 0): fg = QColor(192, 192, 192) # save self._markedItemsOriginalForeground.append(fg) def _setHighlight(self, item): row = item.row() highlightedItemsForeground = [] highlightedItemsBackground = [] self._highlightedItems.append( [item, highlightedItemsForeground, highlightedItemsBackground]) for col in range(self.columnCount()): if self.__index and col == 0: highlightedItemsForeground.append(None) highlightedItemsBackground.append(None) continue item = self.item(row, col) fg = item.foreground().color() bg = item.background() # only change qdarkstyle default foreground if fg == QColor(0, 0, 0) or fg == QColor(192, 192, 192): item.setForeground(QColor(0, 0, 0)) # for qdarkstyle default foreground if fg == QColor(0, 0, 0): fg = QColor(192, 192, 192) item.setBackground(self.highlightBackground) # save highlightedItemsForeground.append(fg) highlightedItemsBackground.append(bg) def _resetMark(self): row = self._markedItem.row() self.setRowBackground(row, self._markedItemOriginalBackground) for col in range(self.columnCount()): if self.__index and col == 0: continue item = self.item(row, col) # 如果标记后添加列,可能会导致超出 if col < len(self._markedItemsOriginalForeground): item.setForeground(self._markedItemsOriginalForeground[col]) def _resetHighlight(self, highlightItem): row = highlightItem[0].row() for col in range(self.columnCount()): if self.__index and col == 0: continue item = self.item(row, col) # 如果标记后添加列,可能会导致超出 if col < len(highlightItem[1]): item.setForeground(highlightItem[1][col]) # 如果标记后添加列,可能会导致超出 if col < len(highlightItem[2]): item.setBackground(highlightItem[2][col]) def markByData(self, colName, itemData): """ @colName: 指定item所在的列名 @itemData: item的数据 """ col = self._getColPos(colName) if col is None: return for row in range(self.rowCount()): if self[row, col] == itemData: item = self.item(row, col) self._mark(item) break def _highlightSameItemContent(self, item, clearHightlight=True): """ @clearHightlight: 是否清除先前的高亮 """ if item is None: return if clearHightlight: self._clearHighlight() text = item.text() col = item.column() for row in range(self.rowCount()): item = self.item(row, col) if item.text() == text: self._highlight(item, withCancel=False) def _highlight(self, item, withCancel=True): """ @withCancel: True-对已经高亮的item高亮,则清除该高亮 """ if item is None: return row = item.row() if self._markedItem is not None and self._markedItem.row() == row: return # 取消鼠标所在行的高亮 cancelHighlight = False for i, highlightedItem in enumerate(self._highlightedItems): if highlightedItem[0].row() == row: # 已经高亮过了 if not withCancel: return self._resetHighlight(highlightedItem) cancelHighlight = True break if cancelHighlight: del self._highlightedItems[i] return # highlight self._setHighlight(item) def _mark(self, item): if item is None: return if self._isInHighlight(item): return # 取消鼠标所在行的标记 if self._markedItem is not None and self._markedItem.row() == item.row( ): self._resetMark() self._markedItem = None self._markedItemsOriginalForeground = [] return # unmark previous if self._markedItem is not None: self._resetMark() self._markedItem = None self._markedItemsOriginalForeground = [] # save for new mark self._markedItemOriginalBackground = item.background() self._markedItem = item self._setMark() def _markAct(self): item = self.itemAt(self._rightClickPoint) self._mark(item) def _highlightAct(self): item = self.itemAt(self._rightClickPoint) self._highlight(item) def _highlightSameItemContentAct(self): item = self.itemAt(self._rightClickPoint) clearAction, notClearAction = self._highlightSameItemContentActions if notClearAction.isChecked(): notClearAction.setChecked(False) clearHighlight = False else: clearAction.setChecked(False) clearHighlight = True self._highlightSameItemContent(item, clearHighlight) def _initItemMenu(self): """ 初始化Item右键菜单 """ self._itemMenu = QMenu(self) self._tableCountAction = QAction('', self) self._itemMenu.addAction(self._tableCountAction) self._itemMenu.addSeparator() self._autoScrollAction = QAction( '关闭自动滚动' if self._enableAutoScroll else '开启自动滚动', self) self._autoScrollAction.triggered.connect(self._autoScrollAct) self._itemMenu.addAction(self._autoScrollAction) self._markAction = QAction('标记', self) self._markAction.triggered.connect(self._markAct) self._itemMenu.addAction(self._markAction) self._visibleMarkAction = QAction('定位到标记', self) self._visibleMarkAction.triggered.connect(self._visibleMarkAct) self._itemMenu.addAction(self._visibleMarkAction) # item只有一种状态,要不是标记,要不就是高亮 self._highlightAction = QAction('高亮', self) self._highlightAction.triggered.connect(self._highlightAct) self._itemMenu.addAction(self._highlightAction) # 高亮所有同列相同内容的item menu = self._itemMenu.addMenu('高亮同列相同内容的表项') self._highlightSameItemContentActions = [ QAction('清除先前高亮', self), QAction('保留先前高亮', self) ] for action in self._highlightSameItemContentActions: action.triggered.connect(self._highlightSameItemContentAct) action.setCheckable(True) menu.addAction(action) action = QAction('查找...', self) action.triggered.connect(self._findAct) self._itemMenu.addAction(action) def _findAct(self): data = {} if DySingleEditDlg(data, '查找', '要查找的内容').exec_(): text = str(data['data']) self._findItems = self.findItems(text, Qt.MatchContains) self._curFindItemPos = 0 if self._findItems: self.scrollToItem(self._findItems[self._curFindItemPos]) self.setCurrentItem(self._findItems[self._curFindItemPos]) else: QMessageBox.warning(self, '警告', '没有找到要查找的内容!') def keyPressEvent(self, event): if event.key() == Qt.Key_F3: if not self._findItems: QMessageBox.warning(self, '警告', '没有找到要查找的内容!') return self._curFindItemPos += 1 self._curFindItemPos = self._curFindItemPos % len(self._findItems) self.scrollToItem(self._findItems[self._curFindItemPos]) self.setCurrentItem(self._findItems[self._curFindItemPos]) def contextMenuEvent(self, event): """ Item右键点击事件 """ self._rightClickPoint = event.pos() item = self.itemAt(self._rightClickPoint) self._tableCountAction.setText('行: {0}, 列: {1}'.format( self.rowCount(), self.columnCount())) if item is None: self._markAction.setEnabled(False) self._highlightAction.setEnabled(False) else: itemState = 0 # 0: not marked or highlighted, 1: marked, 2: highlighted if self._markedItem is not None and self._markedItem.row( ) == item.row(): itemState = 1 else: for highlightedItem in self._highlightedItems: if highlightedItem[0].row() == item.row(): itemState = 2 if itemState == 0: self._markAction.setText('标记') self._markAction.setEnabled(True) self._highlightAction.setText('高亮') self._highlightAction.setEnabled(True) elif itemState == 1: self._markAction.setText('取消标记') self._markAction.setEnabled(True) self._highlightAction.setEnabled(False) else: self._highlightAction.setText('取消高亮') self._highlightAction.setEnabled(True) self._markAction.setEnabled(False) # at last, set visible mark action if self._markedItem is not None: self._visibleMarkAction.setText('定位标记') self._visibleMarkAction.setEnabled(True) else: self._visibleMarkAction.setEnabled(False) self._itemMenu.popup(QCursor.pos()) def removeRow(self, row): """ remove row, which can be by index or key """ self.setSortingEnabled(False) if isinstance(row, int): # remove by index # find item in map delRowKey = None for rowKey, items in self._itemsMap.items(): if items[0].row() == row: delRowKey = rowKey break # remove from map if delRowKey is not None: del self._itemsMap[delRowKey] # remove from table widget super().removeRow(row) else: # remove by key # remove from map if row in self._itemsMap: delRow = self._itemsMap[row][0].row() del self._itemsMap[row] # remove from table widget super().removeRow(delRow) self.setSortingEnabled(True) def removeAll(self): rowCount = self.rowCount() for _ in range(rowCount): self.removeRow(0) def getAll(self): """ 以列表方式返回table的所有值,Org.列除外 """ tableItems = [] for row in range(self.rowCount()): rowItems = [] colCount = (self.columnCount() - 1) if self.__index else self.columnCount() for col in range(colCount): rowItems.append(self[row, col]) tableItems.append(rowItems) return tableItems def getHighlights(self): """ 以列表方式返回table所有高亮的值,Org.列除外 """ # get sorted highlighed rows highlightedRows = [item[0].row() for item in self._highlightedItems] highlightedRows.sort() tableItems = [] for row in highlightedRows: rowItems = [] colCount = (self.columnCount() - 1) if self.__index else self.columnCount() for col in range(colCount): rowItems.append(self[row, col]) tableItems.append(rowItems) return tableItems def toDataFrame(self): colNames = self.getColNames() rows = self.getAll() df = pd.DataFrame(rows, columns=colNames) return df def getColNames(self): colNames = [] for col in range(self.columnCount()): headerItem = self.horizontalHeaderItem(col) colName = headerItem.text() if colName == 'Org.': continue colNames.append(colName) return colNames if colNames else None def getColName(self, col): if self.__index: col += 1 headerItem = self.horizontalHeaderItem(col) if headerItem: return headerItem.text() return None def getColumnsData(self, colNames): """ 以列表方式返回指定列名的所有值 @colNames: [colName] @return: [[data]] """ # get postions of column names colPos = [self._getColPos(x) for x in colNames] colPos = [((x - 1) if self.__index else x) for x in colPos] tableItems = [] for row in range(self.rowCount()): rowItems = [] for col in colPos: rowItems.append(self[row, col]) tableItems.append(rowItems) return tableItems def appendColumns(self, columnNames, columnsData): """ @columnNames: [column name] @columnsData: [[column data]] """ self.setSortingEnabled(False) # adjust start column postion for appended columns colStart = (self.columnCount() - 1) if self.__index else self.columnCount() # append column names for col, name in enumerate(columnNames, colStart): self.addColName(col, name) # append columns data for row, rowData in enumerate(columnsData): for col, data in enumerate(rowData, colStart): self._update((row, col), data) self.resizeColumnsToContents() self.resizeRowsToContents() # 重新设置标记 self._renewMark() # 重新设置高亮 self._renewHighlight() self.setSortingEnabled(True) def _updateAutoForegroundColForeground(self, row): item = self.item(row, self._autoForegroundCol) if item is None: return try: value = float(item.data(self._role)) except Exception as ex: value = 0 # if referenced item doesn't have value or not number, think it as default 0. if value > 0: color = Qt.red elif value < 0: color = Qt.darkGreen else: if item.background() == Qt.white: # for qdarkstyle color = Qt.black else: color = QColor('#C0C0C0') item.setForeground(color) def updateAutoForegroundCol(self, colAbs): """ @colAbs: 更新自动前景色关键列,包含'Org.' column """ if isinstance(colAbs, str): self._autoForegroundCol = self._getColPos(colAbs) else: self._autoForegroundCol = colAbs if self._autoForegroundCol is None: return for row in range(self.rowCount()): # upate foreground of auto foreground column item self._updateAutoForegroundColForeground(row) refItem = self.item(row, self._autoForegroundCol) if refItem is None: continue for col in range(self.columnCount()): if self.__index and col == 0: # ignore 'Org.' column continue item = self.item(row, col) if item is None: continue item.setForeground(refItem.foreground()) def getForegroundOverride(self, value): """ 可由子类重载,这样可以根据不同的值设置不同的前景色 """ try: value = float(value) if value > 0: color = Qt.red elif value < 0: color = Qt.darkGreen else: color = None # default except Exception as ex: color = None return color def _getForeground(self, rowData, autoForegroundCol, item): # 如果@rowData的item个数小于等于@autoForegroundCol # 支持row数据比header少的状况 try: value = rowData[autoForegroundCol] color = self.getForegroundOverride(value) except Exception as ex: color = None if color is None: if item.background() == Qt.white: color = Qt.black else: # for qdarkstyle color = QColor(192, 192, 192) return color def _updateOrg(self, row): if not self.__index: return item = self.item(row, 0) if item is None: item = DyTableWidgetItem(self._role) self.setItem(row, 0, item) item.setData(self._role, row + 1) def clearAllRows(self): self._clearVisualEffects() self.setRowCount(0) self._initRows() def fastAppendRows(self, rows, autoForegroundColName=None, new=False): """ 快速批量添加行数据,忽略细节 调用之前,必须先设置header @new: 新建还是添加 """ self.setSortingEnabled(False) if new: self._clearVisualEffects() self.setRowCount(len(rows)) rowStart = 0 self._initRows() else: rowStart = self.rowCount() self.setRowCount(rowStart + len(rows)) if autoForegroundColName is not None: self._autoForegroundCol = self._getColPos(autoForegroundColName) # column position in input raw data(@rows) if self._autoForegroundCol is not None: autoForegroundCol = self._autoForegroundCol - 1 if self.__index else self._autoForegroundCol offset = 1 if self.__index else 0 item = None for row, rowData in enumerate(rows, rowStart): self._updateOrg(row) for col, value in enumerate(rowData, offset): # create new if not existing item = self.item(row, col) if item is None: item = DyTableWidgetItem(self._role) self.setItem(row, col, item) # set item data self._setItemDataFast(item, value) # set foreground if autoForegroundColName is not None and self._autoForegroundCol is not None: if col == offset: # only get auto foreground when begining of row color = self._getForeground(rowData, autoForegroundCol, item) item.setForeground(color) self.resizeColumnsToContents() self.resizeRowsToContents() self.setSortingEnabled(True) if self._enableAutoScroll and item is not None: self.scrollToItem(item) def fastAppendColumns(self, columnNames, columnsData): """ 快速批量添加列数据,忽略细节 @columnNames: [column name] @columnsData: [[column data]] """ self.setSortingEnabled(False) # adjust start column postion for appended columns colStart = self.columnCount() # append column names self.addColNames(columnNames) # append columns data for row, rowData in enumerate(columnsData): for col, value in enumerate(rowData, colStart): # create new if not existing item = self.item(row, col) if item is None: item = DyTableWidgetItem(self._role) self.setItem(row, col, item) # set item data self._setItemDataFast(item, value) # get item of auto foreground if self._autoForegroundCol is None: continue refItem = self.item(row, self._autoForegroundCol) if not refItem: continue # set forground same as reference item item.setForeground(refItem.foreground()) self.resizeColumnsToContents() self.resizeRowsToContents() # 重新设置标记 self._renewMark() # 重新设置高亮 self._renewHighlight() self.setSortingEnabled(True) def _renewMark(self): """ 重新设置标记 """ markedItem = self._markedItem # 先取消标记 self._mark(markedItem) # 设置标记 self._mark(markedItem) def _renewHighlight(self): """ 重新设置高亮 """ # reproduce highlighed items because during cancel highlight procedure element will be deleted from @self._highlightedItems highlightedItems = [item[0] for item in self._highlightedItems] for item in highlightedItems: # 先取消高亮 self._highlight(item) # 设置高亮 self._highlight(item) def setItemsForeground(self, rowKeys, colors): """ @rowKeys: [rowKey] or [row number] @colors: ((text, color)) or [[text, color]] """ for key in rowKeys: for col in range(self.columnCount()): item = self._getItem(key, col) if item is None: continue itemData = item.data(self._role) for text, color in colors: if isinstance(itemData, str) and text in itemData: item.setForeground(color) break def filter(self, filter, highlight=False): """ 根据filter表达式选取行数据,filter表达式是对列进行操作。对应的列为x[0], x[1], ... @return: 过滤出来的数据列表 """ # 取消高亮 self._clearHighlight() tableItems = [] for row in range(self.rowCount()): rowItems = [] colCount = (self.columnCount() - 1) if self.__index else self.columnCount() for col in range(colCount): rowItems.append(self[row, col]) # execute filter try: x = rowItems if not eval(filter): # some of elements are None continue except Exception as ex: continue if highlight: self._highlight(self.item(row, col)) tableItems.append(rowItems) return tableItems def org(self, row): if self.__index: return self[row, 'Org.'] return None def addColumnOperateColumns(self, exp): """ 根据exp表达式进行列运算(类似于Pandas),并添加列到table widget x代表table widget对应的DataFrame """ newColumnData = [] for row in range(self.rowCount()): rowItems = [] colCount = (self.columnCount() - 1) if self.__index else self.columnCount() for col in range(colCount): rowItems.append(self[row, col]) # execute exp x = rowItems try: value = eval(exp) except: value = None newColumnData.append([value]) # get column name x = self.getColNames() try: p = re.compile('x\[\d+\]') elements = p.findall(exp) elements_ = [] for v in elements: elements_.append('[' + eval(v) + ']') expFormat = p.sub('{}', exp) newColumnName = expFormat.format(*elements_) except: newColumnName = exp # add columns into table widget self.fastAppendColumns([newColumnName], newColumnData)
class GUI(QtWidgets.QMainWindow): def __init__(self): '''Asetetaan muuttujille alkuarvoja ohjelman suorittamiseksi''' super().__init__() self.title = "Lujuusanalysaattori" self.left = 200 self.top = 200 self.width = 1300 self.height = 700 self.palkin_default_pituus = 5 self.square_size = 10 self.ikkuna() self.button_height = 75 self.button_width = 150 self.button_separation = 25 self.x = 0 self.y = 0 self.palkin_leveys = 700 self.palkin_korkeus = 75 self.palkin_keskipiste = 650 self.palkin_paatypiste = 1000 self.yksikko_arvo = 0 self.voima = 20 self.maks_jannitys = "-" self.asteikko_teksti = QGraphicsSimpleTextItem() '''Lisää QGraphicsScenen ruudukon piirtämistä varten''' self.scene = QtWidgets.QGraphicsScene() self.scene.setSceneRect(0, -20, self.width - 200, self.height - 100) '''Suoritetaan lukuisia metodeja, jolla ohjelma "alustetaan"''' self.aloita_simulaatio() self.simulaatioikkuna() self.simulaatio_nappi() self.materiaali_valikko() self.uusi_palkki_nappi() self.lisaa_tuki_nappi() self.lisaa_ulkoinen_voima_nappi() self.poista_ulkoinen_voima_nappi() self.vaihda_tuki_nappi() Ominaisuudet.alkuarvot(self) self.lisaa_palkki() self.palkin_pituus_valikko() self.yksikko_pituus() self.asteikko() self.lisaa_asteikko_arvo() self.asteikko_teksti.hide() self.tulos_teksti() self.lisaa_seina_tuki() self.lisaa_tuki_alhaalta() self.ulkoinen_voima_valikko() self.ulkoinen_voima_nuoli_alatuki() self.ulkoinen_voima_nuoli_seinatuki() Ominaisuudet.alkuarvot(self) '''Asetetaan tietyille napeille tietty näkyvyys''' self.lisaa_tuki.setEnabled(False) self.simuloi.setEnabled(False) self.show() def ikkuna(self): '''Tekee ohjelman pääikkunan''' self.setGeometry(self.left, self.top, self.width, self.height) self.setWindowTitle('Lujuusanalysaattori') self.horizontal = QtWidgets.QHBoxLayout() '''Luo menubarin''' self.uusiAction = QAction("Uusi simulaatio", self) self.uusiAction.setStatusTip("Luo uusi rakenne") self.uusiAction.triggered.connect(self.uusi_rakenne) self.uusiAction.setEnabled(True) self.uusiAction.setShortcut("Ctrl+N") self.tallennaAction = QAction("Tallenna simulaatio", self) self.tallennaAction.setStatusTip("Tallenna simulaatio") self.tallennaAction.triggered.connect(self.tallenna_rakenne) self.tallennaAction.setEnabled(False) self.tallennaAction.setShortcut("Ctrl+S") self.avaaAction = QAction("Lataa simulaatio", self) self.avaaAction.setStatusTip("Lataa simulaatio tiedostosta") self.avaaAction.triggered.connect(self.lataa_tallennettu_rakenne) self.avaaAction.setShortcut("Ctrl+O") self.exitAction = QAction("Exit", self) self.exitAction.setToolTip("Lopeta ohjelma") self.exitAction.triggered.connect(self.close_application) self.exitAction.setShortcut("Ctrl+E") self.statusBar() mainMenu = self.menuBar() fileMenu = mainMenu.addMenu('&File') aboutMenu = mainMenu.addMenu('&About') fileMenu.addAction(self.uusiAction) fileMenu.addAction(self.avaaAction) fileMenu.addAction(self.tallennaAction) fileMenu.addAction(self.exitAction) def tallenna_rakenne(self): '''Hoitaa rakenteen tallentamisen''' tallennus = Tallennin.tallenin(self) if tallennus == True: '''Kerrotaan käyttäjälle, että tallennus onnistui''' msgBox = QMessageBox() msgBox.setText("Tallennus onnistui!") msgBox.setWindowTitle("Onnistunut Tallennus") msgBox.setMinimumWidth(50) msgBox.addButton(QPushButton('OK'), QMessageBox.NoRole) msgBox.exec_() def lataa_tallennettu_rakenne(self): '''Metodi avaa QFileDialog ikkunan, josta käyttäjä valitsee tiedoston, jossa aiemmin tallennettu rakenne sijaitsee. Vain .txt -tiedostot ovat ladattavissa ''' options = QFileDialog.Options() options |= QFileDialog.DontUseNativeDialog tiedosto, _ = QFileDialog.getOpenFileName(self, "Valitse tiedosto", "", "txt Files (*.txt)", options=options) lataus = Lataaja.lataaja(self, tiedosto) if lataus == False: return if lataus == True: self.uusi_rakenne() Lataaja.lataaja(self, tiedosto) tuen_tyyppi = Ominaisuudet.palauta_tuen_tyyppi(self) '''Jos tuki on seinästä, piirretään sitä vastaava grafiikka''' if tuen_tyyppi == 0: self.nayta_seina_tuki() self.gradient_seina_tuki() '''Jos tuki on alhaalta, piirretään sitä vastaava grafiikka''' if tuen_tyyppi == 1: self.nayta_tuki_alhaalta() self.gradient_alatuki() if tuen_tyyppi != 2: self.vaihda_tuki.show() self.lisaa_tuki.hide() '''Jos ulkoinen voima on asetettu, piirretään se''' ulkoinen_voima = int( Ominaisuudet.onko_ulkoinen_voima_asetettu(self)) if ulkoinen_voima == 1: self.nayta_ulkoinen_voima() self.nayta_palkki() Laskin.laskin(self) self.paivita_tulos_teksti() self.tulos.show() self.sp.setValue(float(Ominaisuudet.palauta_palkin_pituus(self))) self.uusiAction.setEnabled(True) self.simuloi.setEnabled(True) '''Kerrotaan käyttäjälle, että kaikki onnistui''' msgBox = QMessageBox() msgBox.setText("Lataus onnistui!") msgBox.setWindowTitle("Onnistunut lataus") msgBox.addButton(QPushButton('OK'), QMessageBox.NoRole) msgBox.exec_() def aloita_simulaatio(self): '''Aloittaa simulaation''' self.setCentralWidget(QtWidgets.QWidget()) self.horizontal = QtWidgets.QHBoxLayout() self.centralWidget().setLayout(self.horizontal) def simulaatioikkuna(self): '''lisää view näyttämistä varten''' self.view = QtWidgets.QGraphicsView(self.scene, self) self.view.adjustSize() self.view.show() self.horizontal.addWidget(self.view) def uusi_palkki_nappi(self): '''Luo Uusi palkki -napin''' self.uusi_palkki = QPushButton('Uusi palkki') self.uusi_palkki.setToolTip("Lisää uusi palkki") self.uusi_palkki.move(0, 0) self.uusi_palkki.resize(self.button_width, self.button_height) self.uusi_palkki.font = QtGui.QFont() self.uusi_palkki.font.setPointSize(12) self.uusi_palkki.setFont(self.uusi_palkki.font) self.uusi_palkki.setEnabled(True) self.scene.addWidget(self.uusi_palkki) self.uusi_palkki.clicked.connect(self.nayta_palkki) def nayta_palkki(self): '''Näyttää kaikki palkkiin liittyvät komponentit sekä asettaa uusi palkki -napin toimimattomaksi''' self.rect.show() self.palkin_pituus.show() self.sp.show() self.yksikko.show() self.asteikko_teksti.show() self.line.show() self.nuoli_1.show() self.nuoli_2.show() self.uusi_palkki.setEnabled(False) self.lisaa_tuki.setEnabled(True) self.materiaali_valinta.setEnabled(True) def lisaa_palkki(self): '''lisää palkin''' self.rect = QGraphicsRectItem(300, 200, self.palkin_leveys, self.palkin_korkeus) self.rect.setBrush(QBrush(4)) self.scene.addItem(self.rect) self.rect.hide() self.lisaa_tuki.setEnabled(True) '''Aina kun on uusi palkki luotu, voidaan aloittaa simulaatio alusta''' self.uusiAction.setEnabled(True) def lisaa_tuki_nappi(self): '''Luo Lisää tuki -napin''' self.lisaa_tuki = QPushButton("Lisää tuki") self.lisaa_tuki.setToolTip("Lisää tuki") self.lisaa_tuki.move(0, self.button_height + self.button_separation) self.lisaa_tuki.resize(self.button_width, self.button_height) self.lisaa_tuki.font = QtGui.QFont() self.lisaa_tuki.font.setPointSize(12) self.lisaa_tuki.setFont(self.lisaa_tuki.font) self.lisaa_tuki.setEnabled(False) self.lisaa_tuki.clicked.connect(self.valitse_tuki) self.scene.addWidget(self.lisaa_tuki) def vaihda_tuki_nappi(self): '''Luo vaihda tuki -napin''' self.vaihda_tuki = QPushButton("Vaihda tuki") self.vaihda_tuki.setToolTip("Vaihda tuki") self.vaihda_tuki.move(0, self.button_height + self.button_separation) self.vaihda_tuki.resize(self.button_width, self.button_height) self.vaihda_tuki.setFont(self.lisaa_tuki.font) self.vaihda_tuki.clicked.connect(self.valitse_tuki) self.scene.addWidget(self.vaihda_tuki) self.vaihda_tuki.hide() def valitse_tuki(self): '''Tuen valinta. Jos tuki on seinästä (tyyppi = 0), kysytään halutaanko vaihtaa. Jos haluaa muutetaan tuen grafiikka ja arvo''' if Ominaisuudet.palauta_tuen_tyyppi(self) == 0: msgBox = QMessageBox() msgBox.setText("Haluatko vaihtaa tuen tyyppiä?") msgBox.addButton(QPushButton('En'), QMessageBox.NoRole) msgBox.addButton(QPushButton('Kyllä'), QMessageBox.YesRole) vastaus = msgBox.exec_() self.rect.setBrush(QBrush(4)) if vastaus == 1: self.viiva_1.hide() self.viiva_2.hide() self.viiva_3.hide() self.viiva_4.hide() self.nayta_tuki_alhaalta() if int(Ominaisuudet.onko_ulkoinen_voima_asetettu(self)) == 1: self.viiva.hide() self.nuoli_3.hide() self.viiva_5.show() self.nuoli_6.show() Ominaisuudet.tuki(self, 1) return '''Jos tuki on alhaalta (tyyppi = 1), kysytään halutaanko vaihtaa. Jos haluaa muutetaan tuen grafiikka ja arvo''' if Ominaisuudet.palauta_tuen_tyyppi(self) == 1: msgBox = QMessageBox() msgBox.setText("Haluatko vaihtaa tuen tyyppiä?") msgBox.addButton(QPushButton('Kyllä'), QMessageBox.YesRole) msgBox.addButton(QPushButton('En'), QMessageBox.NoRole) vastaus = msgBox.exec_() self.rect.setBrush(QBrush(4)) if vastaus == 0: Ominaisuudet.tuki(self, 0) self.nuoli_4.hide() self.nuoli_5.hide() self.nayta_seina_tuki() if int(Ominaisuudet.onko_ulkoinen_voima_asetettu(self)) == 1: self.viiva.show() self.nuoli_3.show() self.viiva_5.hide() self.nuoli_6.hide() if vastaus == 1: pass '''Jos tukea ei ole (tyyppi = 2). Tuen tyypin valinta''' if Ominaisuudet.palauta_tuen_tyyppi(self) == 2: msgBox = QMessageBox() msgBox.setText("Valitse tuen tyyppi") msgBox.addButton(QPushButton('Seinätuki'), QMessageBox.YesRole) msgBox.addButton(QPushButton('Tuki alhaalta'), QMessageBox.NoRole) vastaus = msgBox.exec_() self.vaihda_tuki.show() self.lisaa_tuki.hide() if vastaus == 0: self.nayta_seina_tuki() Ominaisuudet.tuki(self, 0) if vastaus == 1: self.nayta_tuki_alhaalta() Ominaisuudet.tuki(self, 1) '''Joka tapauksessa asetetaan ulkoisen voiman lisääminen mahdolliseksi sekä maalataan palkki normaaliksi''' self.lisaa_ulkoinen_voima.setEnabled(True) self.simuloi.setEnabled(True) def nayta_seina_tuki(self): '''Näytetään seinätukea kuvaavat grafiikat''' self.viiva_1.show() self.viiva_2.show() self.viiva_3.show() self.viiva_4.show() def nayta_tuki_alhaalta(self): '''Näytetään alatukea kuvaavat grafiikat''' self.nuoli_4.show() self.nuoli_5.show() def paivita_tuen_tyyppi(self, tyyppi): '''Päivittää tuen tyypin arvon Ominaisuudet luokassa''' Ominaisuudet.tuki(self, tyyppi) def lisaa_seina_tuki(self): '''Piirtää seinätukea kuvaavat viivat sekä asettaa self.tuen_tyyppi arvoksi Asettaa SIMULOI-napin painettavaksi''' viiva = QtGui.QPen(QtCore.Qt.black, 2) viiva.setStyle(QtCore.Qt.SolidLine) self.viiva_1 = QGraphicsLineItem(QtCore.QLineF(300, 202, 275, 225)) self.viiva_2 = QGraphicsLineItem(QtCore.QLineF(300, 222, 275, 245)) self.viiva_3 = QGraphicsLineItem(QtCore.QLineF(300, 242, 275, 265)) self.viiva_4 = QGraphicsLineItem(QtCore.QLineF(300, 262, 275, 285)) self.scene.addItem(self.viiva_1) self.scene.addItem(self.viiva_2) self.scene.addItem(self.viiva_3) self.scene.addItem(self.viiva_4) self.viiva_1.hide() self.viiva_2.hide() self.viiva_3.hide() self.viiva_4.hide() tyyppi = 0 Ominaisuudet.tuki(self, tyyppi) self.simuloi.setEnabled(True) def lisaa_tuki_alhaalta(self): '''Piirtää alhaalta tukemista kuvaavat grafiikat sekä asettaa self.tuen_tyyppi arvoksi 1''' leveys = 15 #nuolen leveus pikseleissä korkeus = 30 #nuuolen korkeus pikseleissä '''Nuolen kärkien koordinaatit''' nuoli_piste_1 = QtCore.QPointF(305, 275) nuoli_piste_2 = QtCore.QPointF(305 - leveys, 275 + korkeus) nuoli_piste_3 = QtCore.QPointF(305 + leveys, 275 + korkeus) nuoli_piste_4 = QtCore.QPointF(995, 275) nuoli_piste_5 = QtCore.QPointF(995 - leveys, 275 + korkeus) nuoli_piste_6 = QtCore.QPointF(995 + leveys, 275 + korkeus) '''Luodaan nuolia kuvaavat QPolygonF oliot''' self.nuoli_4 = QGraphicsPolygonItem( QtGui.QPolygonF([nuoli_piste_1, nuoli_piste_2, nuoli_piste_3])) self.nuoli_5 = QGraphicsPolygonItem( QtGui.QPolygonF([nuoli_piste_4, nuoli_piste_5, nuoli_piste_6])) self.nuoli_brush = QtGui.QBrush(1) self.nuoli_pencil = QtGui.QPen(QtCore.Qt.black, 2) self.nuoli_pencil.setStyle(QtCore.Qt.SolidLine) '''Lisätään nuolet sceneen''' self.scene.addItem(self.nuoli_4) self.scene.addItem(self.nuoli_5) self.nuoli_4.hide() self.nuoli_5.hide() tyyppi = 1 Ominaisuudet.tuki(self, tyyppi) self.simuloi.setEnabled(True) def lisaa_ulkoinen_voima_nappi(self): '''Luo Lisää ulkoinen voima -napin''' self.lisaa_ulkoinen_voima = QPushButton("Lisää ulkoinen voima") self.lisaa_ulkoinen_voima.setToolTip("Lisää ulkoinen voima") self.lisaa_ulkoinen_voima.move( 0, 2 * self.button_height + 2 * self.button_separation) self.lisaa_ulkoinen_voima.resize(self.button_width, self.button_height) self.lisaa_ulkoinen_voima.font = QtGui.QFont() self.lisaa_ulkoinen_voima.font.setPointSize(8) self.lisaa_ulkoinen_voima.setFont(self.lisaa_ulkoinen_voima.font) self.lisaa_ulkoinen_voima.clicked.connect(self.nayta_ulkoinen_voima) self.lisaa_ulkoinen_voima.clicked.connect(self.nollaa_gradientti) self.lisaa_ulkoinen_voima.setEnabled(False) self.scene.addWidget(self.lisaa_ulkoinen_voima) def poista_ulkoinen_voima_nappi(self): '''Luo poista ulkoinen voima -napin''' self.poista_ulkoinen_voima = QPushButton("Poista ulkoinen voima") self.poista_ulkoinen_voima.setToolTip("Poista ulkoinen voima") self.poista_ulkoinen_voima.move( 0, 2 * self.button_height + 2 * self.button_separation) self.poista_ulkoinen_voima.resize(self.button_width, self.button_height) self.poista_ulkoinen_voima.setFont(self.lisaa_ulkoinen_voima.font) self.poista_ulkoinen_voima.clicked.connect(self.piilota_ulkoinen_voima) self.poista_ulkoinen_voima.clicked.connect(self.nollaa_gradientti) self.scene.addWidget(self.poista_ulkoinen_voima) self.poista_ulkoinen_voima.hide() def piilota_ulkoinen_voima(self): '''Piilotaa kaiken ulkoiseen voimaan liittyvän''' self.sp_voima.hide() self.yksikko_voima.hide() self.ulkoinen_voima.hide() self.lisaa_ulkoinen_voima.show() self.lisaa_ulkoinen_voima.setEnabled(True) self.viiva.hide() self.nuoli_3.hide() self.viiva_5.hide() self.nuoli_6.hide() self.poista_ulkoinen_voima.hide() self.lisaa_ulkoinen_voima.show() self.tulos.hide() Ominaisuudet.ulkoinen_voima(self, 0) def nayta_ulkoinen_voima(self): '''Näytetään ulkoinen voima riippuen tuen tyypistä''' self.sp_voima.show() self.yksikko_voima.show() self.ulkoinen_voima.show() self.lisaa_ulkoinen_voima.hide() self.poista_ulkoinen_voima.show() if int(Ominaisuudet.palauta_tuen_tyyppi(self)) == 0: self.viiva.show() self.nuoli_3.show() if int(Ominaisuudet.palauta_tuen_tyyppi(self)) == 1: self.viiva_5.show() self.nuoli_6.show() Ominaisuudet.ulkoinen_voima(self, 1) def ulkoinen_voima_valikko(self): '''Luo voiman suuruus -tekstin''' self.ulkoinen_voima = QGraphicsSimpleTextItem("Voiman suuruus") self.ulkoinen_voima.setPos(600, 5) self.ulkoinen_voima.font = QtGui.QFont() self.ulkoinen_voima.font.setPointSize(12) self.ulkoinen_voima.setFont(self.ulkoinen_voima.font) self.lisaa_ulkoinen_voima.setEnabled(False) self.scene.addItem(self.ulkoinen_voima) self.ulkoinen_voima.hide() '''Luo voiman arvon QSpinBoxin''' self.sp_voima = QSpinBox() self.sp_voima.move(750, 5) self.sp_voima.setRange(0, 10000) self.sp_voima.setSingleStep(1) self.sp_voima.setMinimumHeight(30) self.sp_voima.setValue(int(Ominaisuudet.palauta_voima(self))) self.sp_voima.valueChanged.connect(self.paivita_voima) self.scene.addWidget(self.sp_voima) self.sp_voima.hide() '''Luo yksikönvalinta QComboBOxin''' self.yksikko_voima = QComboBox() self.yksikko_voima.addItem("kN", 0) self.yksikko_voima.addItem("N", 1) self.yksikko_voima.move(820, 5) self.yksikko_voima.setMinimumHeight(30) self.yksikko_voima.setCurrentIndex( int(Ominaisuudet.palauta_voiman_yksikko(self))) self.yksikko_voima.setEditable(True) self.yksikko_voima.lineEdit().setAlignment(QtCore.Qt.AlignCenter) self.scene.addWidget(self.yksikko_voima) self.yksikko_voima.currentIndexChanged.connect( self.paivita_yksikko_voima) self.yksikko_voima.hide() def ulkoinen_voima_nuoli_seinatuki(self): '''Luo nuolen osoittamaan ulkoisen voiman paikkaa''' voima_viiva = QtGui.QPen(QtCore.Qt.black, 2) voima_viiva.setStyle(QtCore.Qt.SolidLine) '''Nuolen kärkien koordinaatit seinätuelle''' nuoli_piste_1 = QtCore.QPointF(self.palkin_paatypiste - 7, 185) nuoli_piste_2 = QtCore.QPointF(self.palkin_paatypiste, 200) nuoli_piste_3 = QtCore.QPointF(self.palkin_paatypiste + 7, 185) viiva_x = self.palkin_paatypiste self.viiva = QGraphicsLineItem( QtCore.QLineF(viiva_x, 100, viiva_x, 200)) '''Luodaan nuoli QPolygonItem olio''' self.nuoli_3 = QGraphicsPolygonItem( QtGui.QPolygonF([nuoli_piste_1, nuoli_piste_2, nuoli_piste_3])) self.nuoli_brush = QtGui.QBrush(1) self.nuoli_pencil = QtGui.QPen(QtCore.Qt.black, 2) self.nuoli_pencil.setStyle(QtCore.Qt.SolidLine) '''Lisätään viiva sekä päiden nuolet sceneen''' self.scene.addItem(self.viiva) self.scene.addItem(self.nuoli_3) self.viiva.hide() self.nuoli_3.hide() '''Lisätään tieto, että voima on asetettu''' Ominaisuudet.ulkoinen_voima(self, 1) def ulkoinen_voima_nuoli_alatuki(self): '''Nuolen kärkien koordinaatit alhaalta tuetulle palkille''' nuoli_piste_1 = QtCore.QPointF(self.palkin_keskipiste - 7, 185) nuoli_piste_2 = QtCore.QPointF(self.palkin_keskipiste, 200) nuoli_piste_3 = QtCore.QPointF(self.palkin_keskipiste + 7, 185) viiva_x = self.palkin_keskipiste '''Luodaan nuoli QPolygonItem olio''' self.nuoli_6 = QGraphicsPolygonItem( QtGui.QPolygonF([nuoli_piste_1, nuoli_piste_2, nuoli_piste_3])) self.nuoli_brush = QtGui.QBrush(1) self.nuoli_pencil = QtGui.QPen(QtCore.Qt.black, 2) self.nuoli_pencil.setStyle(QtCore.Qt.SolidLine) self.viiva_5 = QGraphicsLineItem( QtCore.QLineF(viiva_x, 100, viiva_x, 200)) '''Lisätään viiva sekä päiden nuolet sceneen''' self.scene.addItem(self.viiva_5) self.scene.addItem(self.nuoli_6) self.viiva_5.hide() self.nuoli_6.hide() '''Lisätään tieto, että voima on asetettu''' Ominaisuudet.ulkoinen_voima(self, 1) def paivita_voima(self): '''Lukee voiman arvon ja kutsuu Ominaisuudet luoka metodia voima''' voima = self.sp_voima.value() Ominaisuudet.voima(self, voima) def paivita_yksikko_voima(self): '''Lukee ykiskön arvon ja kutsuu Ominaisuudet-luokan metodia yksikko_voima''' self.yksikko_voima_arvo = self.yksikko_voima.currentData() Ominaisuudet.yksikko_voima(self, self.yksikko_voima_arvo) def materiaali_valikko(self): ''' Luo Materiaali-otsikon''' self.materiaali = QGraphicsSimpleTextItem("Materiaali") self.materiaali.setPos( 0, 3 * self.button_height + 3 * self.button_separation) self.materiaali.font = QtGui.QFont() self.materiaali.font.setPointSize(12) self.materiaali.setFont(self.materiaali.font) self.scene.addItem(self.materiaali) '''Luo drop down valikon materiaalivalinnalle''' self.materiaali_valinta = QComboBox() self.materiaali_valinta.addItem("Teräs", 0) self.materiaali_valinta.addItem("Alumiini", 1) self.materiaali_valinta.addItem("Muovi", 2) self.materiaali_valinta.move( 0, 3 * self.button_height + 3 * self.button_separation + 25) self.materiaali_valinta.resize(self.button_width, self.button_height - 25) self.materiaali_valinta.setEditable(True) self.materiaali_valinta.lineEdit().setAlignment(QtCore.Qt.AlignCenter) self.materiaali_valinta.setCurrentIndex(0) self.scene.addWidget(self.materiaali_valinta) self.materiaali_valinta.setEnabled(False) self.materiaali_valinta.currentIndexChanged.connect( self.paivita_materiaali) def paivita_materiaali(self): '''Lukee materiaalin arvon ja kutsuu Ominaisuudet-luokan metodia materiaali''' materiaali = self.materiaali_valinta.currentData() Ominaisuudet.materiaali(self, materiaali) def simulaatio_nappi(self): '''Luo SIMULOI-napin''' self.simuloi = QPushButton('SIMULOI') self.simuloi.setToolTip('Simuloi valittu rakenne') self.simuloi.move(0, 4 * self.button_height + 4 * self.button_separation) self.simuloi.setStyleSheet("background-color:rgb(122, 201, 255)") self.simuloi.resize(self.button_width, self.button_height) self.simuloi.font = QtGui.QFont() self.simuloi.font.setPointSize(12) self.simuloi.setFont(self.simuloi.font) self.simuloi.setEnabled(False) self.simuloi.clicked.connect(self.simulaatio) self.scene.addWidget(self.simuloi) def simulaatio(self): '''Kutsuu laskentaa suorittavaa metodia ja tallentaa tuloksen. Tämän jälkeen kutsuu lopputuloksen esittävän tekstin päivittävää metodia sekä palkin visualisoivaa gradient-metodia''' Laskin.laskin(self) Ominaisuudet.palauta_tulos(self) self.paivita_tulos_teksti() self.tallennaAction.setEnabled(True) if Ominaisuudet.palauta_tuen_tyyppi(self) == 0: if Ominaisuudet.onko_ulkoinen_voima_asetettu(self) == 1: self.gradient_seina_tuki() if Ominaisuudet.onko_ulkoinen_voima_asetettu(self) == 0: self.gradient_seina_tuki_ei_voimaa() if Ominaisuudet.palauta_tuen_tyyppi(self) == 1: self.gradient_alatuki() def tulos_teksti(self): '''Lisää tekstin, joka kertoo maksimijänintyksen arvon''' teksti = "Maksimijännitys " + str(self.maks_jannitys) + " MPa" self.tulos = QGraphicsSimpleTextItem(teksti) self.tulos.setPos(550, 500) self.tulos.font = QtGui.QFont() self.tulos.font.setPointSize(12) self.tulos.setFont(self.tulos.font) self.scene.addItem(self.tulos) self.tulos.hide() def paivita_tulos_teksti(self): '''Päivittää maksimijännityksen arvoa kuvaavan tekstin''' maks_jannitys = Ominaisuudet.palauta_tulos(self) self.tulos.setText("Maksimijännitys " + str(maks_jannitys) + " MPa") self.tulos.show() def palkin_pituus_valikko(self): '''Luo palkin pituus tekstin sekä spinbox-valitsimen pituuden asettamista varten Päivittää palkin pituuden Ominaisuudet luokan avulla''' self.palkin_pituus = QGraphicsSimpleTextItem("Palkin pituus") self.palkin_pituus.setPos(300, 5) self.palkin_pituus.font = QtGui.QFont() self.palkin_pituus.font.setPointSize(12) self.palkin_pituus.setFont(self.palkin_pituus.font) self.scene.addItem(self.palkin_pituus) self.palkin_pituus.hide() self.sp = QSpinBox() self.scene.addWidget(self.sp) self.sp.hide() self.sp.move(450, 5) self.sp.setRange(0, 100) self.sp.setSingleStep(1) self.sp.setMinimumHeight(30) self.sp.setValue(int(Ominaisuudet.palauta_palkin_pituus(self))) self.paivita_pituus() self.sp.valueChanged.connect(self.paivita_pituus) def paivita_pituus(self): '''Lukee palkin pituuden ja aktivoi Ominaisuudet luokan meodin palkin pituus''' self.palkin_pituus_arvo = self.sp.value() Ominaisuudet.palkin_pituus(self, self.palkin_pituus_arvo) self.paivita_asteikon_arvot() def yksikko_pituus(self): '''Luo yksikönvalinta dropdown-menun ja arvon muuttuessa päivittää yksikön Ominaisuudet-luokassa''' self.yksikko = QComboBox() self.yksikko.addItem("m", 0) self.yksikko.addItem("cm", 1) self.yksikko.addItem("mm", 2) self.yksikko.move(500, 5) self.yksikko.setMinimumHeight(30) self.yksikko.setEditable(True) self.yksikko.lineEdit().setAlignment(QtCore.Qt.AlignCenter) self.yksikko.setCurrentIndex( Ominaisuudet.palauta_pituuden_yksikko(self)) self.scene.addWidget(self.yksikko) self.yksikko.hide() self.yksikko_arvo = self.yksikko.currentData() self.yksikko.currentIndexChanged.connect(self.paivita_yksikko) def paivita_yksikko(self): '''Lukee yksikön arvon ja kutsuu Ominaisuudet-luokan metodia yksikko''' self.yksikko_arvo = self.yksikko.currentData() Ominaisuudet.yksikko(self, self.yksikko_arvo) self.paivita_asteikon_arvot() def asteikko(self): ''''Luodaan viivaa kuvaava olio''' viiva = QtGui.QPen(QtCore.Qt.black, 2) viiva.setStyle(QtCore.Qt.SolidLine) '''Oikean puoleisen nuolen kärkien koordinaatit''' nuoli_1_piste_1 = QtCore.QPointF(990, 390) nuoli_1_piste_2 = QtCore.QPointF(1000, 400) nuoli_1_piste_3 = QtCore.QPointF(990, 410) '''Vasemman puoleisen nuolen kärkien koordinaatit''' nuoli_2_piste_1 = QtCore.QPointF(310, 390) nuoli_2_piste_2 = QtCore.QPointF(300, 400) nuoli_2_piste_3 = QtCore.QPointF(310, 410) '''Luodaan nuoli QPolygonF oliot''' self.nuoli_1 = QGraphicsPolygonItem( QtGui.QPolygonF( [nuoli_1_piste_1, nuoli_1_piste_2, nuoli_1_piste_3])) self.nuoli_2 = QGraphicsPolygonItem( QtGui.QPolygonF( [nuoli_2_piste_1, nuoli_2_piste_2, nuoli_2_piste_3])) self.nuoli_brush = QtGui.QBrush(1) self.nuoli_pencil = QtGui.QPen(QtCore.Qt.black, 2) self.nuoli_pencil.setStyle(QtCore.Qt.SolidLine) self.line = QGraphicsLineItem(QtCore.QLineF(300, 400, 1000, 400)) '''Lisätään viiva sekä päiden nuolet sceneen''' self.scene.addItem(self.line) self.scene.addItem(self.nuoli_1) self.scene.addItem(self.nuoli_2) self.line.hide() self.nuoli_1.hide() self.nuoli_2.hide() def lisaa_asteikko_arvo(self): '''Lisää tekstikentän pituuden arvolle sekä yksikölle''' teksti = (str(Ominaisuudet.palauta_palkin_pituus(self)) + " " + "m") self.asteikko_teksti = QGraphicsSimpleTextItem() self.asteikko_teksti.setText(teksti) self.asteikko_teksti.setPos(650, 425) self.asteikko_teksti.font = QtGui.QFont() self.asteikko_teksti.font.setPointSize(12) self.asteikko_teksti.setFont(self.asteikko_teksti.font) self.scene.addItem(self.asteikko_teksti) self.asteikko_teksti.hide() def paivita_asteikon_arvot(self): '''Päivittää palkin pituutta kuvaavan asteikon''' yksikko = Ominaisuudet.palauta_pituuden_yksikko(self) if yksikko == 0: self.yksikko_merkki = "m" if yksikko == 1: self.yksikko_merkki = "cm" if yksikko == 2: self.yksikko_merkki = "mm" pituus = float(Ominaisuudet.palauta_palkin_pituus(self)) teksti = str(str(pituus) + " " + self.yksikko_merkki) self.asteikko_teksti.setText(teksti) self.asteikko_teksti.show() def gradient_seina_tuki(self): '''Luo seinästä tuetun palkin rasitusta kuvaavan gradientin''' gradient = QLinearGradient(300, 200, 300 + self.palkin_leveys, 200) gradient.setColorAt(0, QColor(244, 72, 66)) gradient.setColorAt(1, QColor(65, 244, 83)) self.rect.setBrush(gradient) def gradient_seina_tuki_ei_voimaa(self): '''Luo ilman ulkoista voimaa olevan gradientin''' gradient = QLinearGradient(300, 200, 300 + (self.palkin_leveys / 2), 200) gradient.setColorAt(0, QColor(244, 72, 66)) gradient.setColorAt(1, QColor(65, 244, 83)) self.rect.setBrush(gradient) def gradient_alatuki(self): '''Luo kahdella alatuella olevan palkin rasitusta kuvaavan gradientin''' gradient = QLinearGradient(300, 200, 300 + self.palkin_leveys, 200) gradient.setColorAt(0, QColor(65, 244, 83)) gradient.setColorAt(0.5, QColor(244, 72, 66)) gradient.setColorAt(1, QColor(65, 244, 83)) self.rect.setBrush(gradient) def nollaa_gradientti(self): '''Asettaa palkin "normaaliksi"''' self.rect.setBrush(QBrush(4)) def uusi_rakenne(self): '''Muokkaa ikkunaa uuden simulaation luomista varten''' self.rect.hide() self.ulkoinen_voima.hide() self.sp_voima.hide() self.yksikko_voima.hide() self.nuoli_1.hide() self.nuoli_2.hide() self.nuoli_3.hide() self.nuoli_4.hide() self.nuoli_5.hide() self.nuoli_6.hide() self.viiva_1.hide() self.viiva_2.hide() self.viiva_3.hide() self.viiva_4.hide() self.viiva_5.hide() self.viiva.hide() self.palkin_pituus.hide() self.sp.hide() self.yksikko.hide() self.line.hide() self.asteikko_teksti.hide() self.tulos.hide() self.nollaa_gradientti() self.lisaa_tuki.show() self.vaihda_tuki.hide() self.poista_ulkoinen_voima.hide() self.lisaa_ulkoinen_voima.show() Ominaisuudet.alkuarvot(self) '''Asettaa napit''' self.uusi_palkki.setEnabled(True) self.lisaa_ulkoinen_voima.setEnabled(False) self.lisaa_tuki.setEnabled(False) self.simuloi.setEnabled(False) self.tallennaAction.setEnabled(False) '''Päivittää tuen tyypiksi arvon, joka vastaa, ettei tukea ole''' self.tuen_tyyppi = 2 def close_application(self): '''sulkee ohjelman''' sys.exit()
class MainWin(QMainWindow): def __init__(self, fileName=None, logName=None, parent=None): super(MainWin, self).__init__(parent) #self.setWindowIcon(QIcon(':/images/logo.png')) self.setToolButtonStyle(Qt.ToolButtonFollowStyle) self.setupFileActions() self.setupEditActions() self.setupTextActions() self.setupRunActions() self.initializeSettings() self.populateRunSettings() # FIXME put in initializeSettings()? settingsMenu = QMenu('Settings', self) self.menuBar().addMenu(settingsMenu) settingsMenu.addAction('Configure...', self.configure) helpMenu = QMenu("Help", self) self.menuBar().addMenu(helpMenu) helpMenu.addAction("About", self.about) helpMenu.addAction("About &Qt", QApplication.instance().aboutQt) self.splitter = QSplitter(self) self.splitter.setOrientation(Qt.Vertical) self.textPane = TextPane() self.logPane = LogPane() self.logBox = QGroupBox() self.logBox.setFlat(True) vbox = QVBoxLayout() vbox.addWidget(self.logPane) self.logBox.setLayout(vbox) self.splitter.addWidget(self.textPane) self.splitter.addWidget(QLabel()) # spacer self.splitter.addWidget(self.logBox) self.setCentralWidget(self.splitter) self.loadSrc(fileName) self.loadLog(logName) #if logName and (-1 == self.comboLogFile.findText(logName)): #self.comboLogFile.addItem(logName) self.logPane.setFocus() self.fontChanged(self.textPane.font()) self.textPane.document().modificationChanged.connect(self.actionSave.setEnabled) self.textPane.document().modificationChanged.connect(self.setWindowModified) self.textPane.document().undoAvailable.connect(self.actionUndo.setEnabled) self.textPane.document().redoAvailable.connect( self.actionRedo.setEnabled) self.setWindowModified(self.textPane.document().isModified()) self.actionSave.setEnabled(self.textPane.document().isModified()) self.actionUndo.setEnabled(self.textPane.document().isUndoAvailable()) self.actionRedo.setEnabled(self.textPane.document().isRedoAvailable()) self.actionUndo.triggered.connect(self.textPane.undo) self.actionRedo.triggered.connect(self.textPane.redo) self.actionCut.setEnabled(False) self.actionCopy.setEnabled(False) self.actionCut.triggered.connect(self.textPane.cut) self.actionCopy.triggered.connect(self.textPane.copy) self.actionPaste.triggered.connect(self.textPane.paste) self.textPane.copyAvailable.connect(self.actionCut.setEnabled) self.textPane.copyAvailable.connect(self.actionCopy.setEnabled) QApplication.clipboard().dataChanged.connect(self.clipboardDataChanged) self.actionRun.triggered.connect(self.scannoCheck) self.logPane.lineMatchChanged.connect(self.logLineMatchChanged) def closeEvent(self, e): if self.maybeSave(): e.accept() else: e.ignore() def setupFileActions(self): tb = QToolBar(self) tb.setWindowTitle("File Actions") self.addToolBar(tb) menu = QMenu("&File", self) self.menuBar().addMenu(menu) self.actionNew = QAction("&New", self, priority=QAction.LowPriority, shortcut=QKeySequence.New, triggered=self.fileNew) tb.addAction(self.actionNew) menu.addAction(self.actionNew) self.actionOpen = QAction("&Open...", self, shortcut=QKeySequence.Open, triggered=self.fileOpen) tb.addAction(self.actionOpen) menu.addAction(self.actionOpen) menu.addSeparator() self.actionSave = QAction("&Save", self, shortcut=QKeySequence.Save, triggered=self.fileSave, enabled=False) tb.addAction(self.actionSave) menu.addAction(self.actionSave) self.actionSaveAs = QAction("Save &As...", self, priority=QAction.LowPriority, shortcut=Qt.CTRL + Qt.SHIFT + Qt.Key_S, triggered=self.fileSaveAs) menu.addAction(self.actionSaveAs) menu.addSeparator() self.actionQuit = QAction("&Quit", self, shortcut=QKeySequence.Quit, triggered=self.close) menu.addAction(self.actionQuit) def setupEditActions(self): tb = QToolBar(self) tb.setWindowTitle("Edit Actions") self.addToolBar(tb) menu = QMenu("&Edit", self) self.menuBar().addMenu(menu) self.actionUndo = QAction("&Undo", self, shortcut=QKeySequence.Undo) tb.addAction(self.actionUndo) menu.addAction(self.actionUndo) self.actionRedo = QAction("&Redo", self, priority=QAction.LowPriority, shortcut=QKeySequence.Redo) tb.addAction(self.actionRedo) menu.addAction(self.actionRedo) menu.addSeparator() self.actionCut = QAction("Cu&t", self, priority=QAction.LowPriority, shortcut=QKeySequence.Cut) tb.addAction(self.actionCut) menu.addAction(self.actionCut) self.actionCopy = QAction("&Copy", self, priority=QAction.LowPriority, shortcut=QKeySequence.Copy) tb.addAction(self.actionCopy) menu.addAction(self.actionCopy) self.actionPaste = QAction("&Paste", self, priority=QAction.LowPriority, shortcut=QKeySequence.Paste, enabled=(len(QApplication.clipboard().text()) != 0)) tb.addAction(self.actionPaste) menu.addAction(self.actionPaste) def setupTextActions(self): tb = QToolBar(self) tb.setWindowTitle("Format Actions") self.addToolBar(tb) tb = QToolBar(self) tb.setAllowedAreas(Qt.TopToolBarArea | Qt.BottomToolBarArea) tb.setWindowTitle("Format Actions") self.addToolBarBreak(Qt.TopToolBarArea) self.addToolBar(tb) self.comboFont = QFontComboBox(tb) tb.addWidget(self.comboFont) self.comboFont.activated[str].connect(self.textFamily) self.comboSize = QComboBox(tb) self.comboSize.setObjectName("comboSize") tb.addWidget(self.comboSize) self.comboSize.setEditable(True) db = QFontDatabase() for size in db.standardSizes(): self.comboSize.addItem('{}'.format(size)) self.comboSize.activated[str].connect(self.textSize) self.comboSize.setCurrentIndex(self.comboSize.findText('{}'.format(QApplication.font().pointSize()))) def setupRunActions(self): tb = QToolBar(self) tb.setWindowTitle("Run Actions") self.addToolBar(tb) menu = QMenu("Run", self) self.menuBar().addMenu(menu) self.actionRun = QAction( "&Run", self, shortcut=Qt.CTRL + Qt.Key_R) tb.addAction(self.actionRun) menu.addAction(self.actionRun) self.comboScannoFile = QComboBox(tb) self.comboScannoFile.setObjectName("comboScannoFile") tb.addWidget(self.comboScannoFile) self.comboScannoFile.setEditable(True) self.comboLogFile= QComboBox(tb) self.comboLogFile.setObjectName("comboLogFile") self.comboLogFile.setEditable(True) tb.addWidget(self.comboLogFile) def populateRunSettings(self): for f in self.scannoFiles(): self.comboScannoFile.addItem(f) if self.defaultScannoFile: idx = self.comboScannoFile.findText(self.defaultScannoFile) self.comboScannoFile.setCurrentIndex(idx) self.comboLogFile.addItem('plog.txt') def loadSrc(self, src): if src: if not self.textPane.load(src): return False self.setCurrentFileName(src) return True def loadLog(self, log): if log: if not self.logPane.load(log): return False self.comboLogFile.clear() self.comboLogFile.addItem(log) else: self.logPane.clear() self.logPane.setEnabled(False) self.logBox.setTitle(self.tr('No log file loaded.')) return True def maybeSave(self): if not self.textPane.document().isModified(): return True ret = QMessageBox.warning(self, 'GuiScannos', 'The document has been modified.\n' 'Do you want to save your changes?', QMessageBox.Save | QMessageBox.Discard | QMessageBox.Cancel) if ret == QMessageBox.Save: return self.fileSave() if ret == QMessageBox.Cancel: return False return True def setCurrentFileName(self, fileName=''): self.fileName = fileName self.textPane.document().setModified(False) if not fileName: shownName = 'untitled.txt' self.actionRun.setEnabled(False) else: shownName = QFileInfo(fileName).fileName() self.actionRun.setEnabled(True) self.setWindowTitle(self.tr('{}[*] - {}'.format(shownName, 'GUI Scannos'))) self.setWindowModified(False) def fileNew(self): if self.maybeSave(): self.textPane.clear() self.loadLog(None) # clears logPane, logBox title, etc self.setCurrentFileName() def fileOpen(self): fn, _ = QFileDialog.getOpenFileName(self, 'Open File...', None, 'Text Files (*.txt);;All Files (*)') if fn: self.loadSrc(fn) self.loadLog(None) # clears logPane, logBox title, etc def fileSave(self): if not self.fileName: return self.fileSaveAs() return self.textpane.save(self.fileName) def fileSaveAs(self): fn, _ = QFileDialog.getSaveFileName(self, "Save as...", None, "text files (*.txt);;All Files (*)") if not fn: return False self.setCurrentFileName(fn) return self.fileSave() def logLineMatchChanged(self): linenum = self.logPane.srcLineNum() col = self.logPane.srcColNum() s = self.logPane.srcScanno() self.textPane.setSelection(linenum, col, len(s)) def textFamily(self, family): """Set font family for text and log panes.""" self.textPane.setFontFamily(family) self.logPane.setFontFamily(family) def textSize(self, pointSize): """Set font size for text and log panes.""" self.textPane.setFontPointSize(pointSize) self.logPane.setFontPointSize(pointSize) def clipboardDataChanged(self): self.actionPaste.setEnabled(len(QApplication.clipboard().text()) != 0) def about(self): QMessageBox.about(self, 'About', 'GUI for ppscannos.') def fontChanged(self, font): self.comboFont.setCurrentIndex(self.comboFont.findText(QFontInfo(font).family())) self.comboSize.setCurrentIndex(self.comboSize.findText('{}'.format(font.pointSize()))) def scannoCheck(self): """Run ppscannos.""" scannodir = os.path.dirname(self.ppscannos) cmd = sys.executable assert(cmd) scannoFile = self.comboScannoFile.currentText() if not scannoFile: scannoFile = self.defaultScannoFile scannoFile = scannodir + '/' + scannoFile src = self.fileName log = self.comboLogFile.currentText() if not log: log = './plog.txt' subprocess.call([cmd, self.ppscannos, '-s' + scannoFile, '-o' + log, '-i' + src]) self.loadLog(log) self.logPane.setEnabled(True) def configure(self): """Configure application settings by way of a dialog.""" dlg = ConfigDialog() if dlg.exec(): self.setPPScannos(dlg.lineEditPPScannos.text()) self.setDefaultScannoFile(dlg.comboScannoFiles.currentText()) settings = QSettings(QApplication.organizationName(), QApplication.applicationName()) settings.setValue('ppscannos', self.ppscannos) settings.setValue('defaultScannoFile', self.defaultScannoFile) def setPPScannos(self, s): self.ppscannos = s self.actionRun.setEnabled(self.ppscannos and os.path.exists(self.ppscannos)) def scannoFiles(self): """Return list of .rc filenames (without path) that are in ppscannos directory.""" if not self.ppscannos: return [] return getRCFilesForDir(os.path.dirname(self.ppscannos)) def setDefaultScannoFile(self, s): self.defaultScannoFile = s valid = False if self.defaultScannoFile and self.ppscannos and os.path.exists(self.ppscannos): if os.path.exists(os.path.dirname(self.ppscannos) + '/' + self.defaultScannoFile): valid = True self.actionRun.setEnabled(valid) def initializeSettings(self): """Load persistent config settings.""" settings = QSettings() s = settings.value('ppscannos', type=str) if not s: # try the default s = os.path.expanduser('~') + '/ppscannos1/ppscannos1.py' #s = os.environ['HOME'] + '/ppscannos1/ppscannos1.py' self.setPPScannos(s) s = settings.value('defaultScannoFile', type=str) if (not s) and self.ppscannos: # try the default lst = getRCFilesForDir(os.path.dirname(self.ppscannos)) if len(lst): # prefer 'regex.rc'; otherwise use the first one s = lst[0] for f in lst: if f == 'regex.rc': s = f break self.setDefaultScannoFile(s)