class RatingDelegate(QStyledItemDelegate): # {{{ def __init__(self, *args, **kwargs): QStyledItemDelegate.__init__(self, *args, **kwargs) self.rf = QFont(rating_font()) self.em = Qt.ElideMiddle delta = 0 if iswindows and sys.getwindowsversion().major >= 6: delta = 2 self.rf.setPointSize(QFontInfo(QApplication.font()).pointSize()+delta) def createEditor(self, parent, option, index): sb = QStyledItemDelegate.createEditor(self, parent, option, index) sb.setMinimum(0) sb.setMaximum(5) sb.setSuffix(' ' + _('stars')) return sb def displayText(self, value, locale): r = int(value) if r < 0 or r > 5: r = 0 return u'\u2605'*r def sizeHint(self, option, index): option.font = self.rf option.textElideMode = self.em return QStyledItemDelegate.sizeHint(self, option, index) def paint(self, painter, option, index): option.font = self.rf option.textElideMode = self.em return QStyledItemDelegate.paint(self, painter, option, index)
def create_winner_dialog(winner, user_choice, rand_choice): dia = QMessageBox() font = QFont() font.setFamily("Arial") font.setPointSize(14) dia.setFont(font) if (user_choice == 1): user_choice = "Rock" elif (user_choice == 2): user_choice = "Paper" elif (user_choice == 3): user_choice = "Scissors" if (rand_choice == 1): rand_choice = "Rock" elif (rand_choice == 2): rand_choice = "Paper" elif (rand_choice == 3): rand_choice = "Scissors" dia.setIcon(QMessageBox.Information) dia.setText("User Picked: " + str(user_choice) + "\n" + "Computer Picked: " + str(rand_choice)) dia.setInformativeText("Winner: " + str(winner)) dia.setWindowTitle("Winner") dia.setStandardButtons(QMessageBox.Ok) dia.exec_()
class RatingDelegate(QStyledItemDelegate): # {{{ def __init__(self, *args, **kwargs): QStyledItemDelegate.__init__(self, *args, **kwargs) self.rf = QFont(rating_font()) self.em = Qt.ElideMiddle delta = 0 if iswindows and sys.getwindowsversion().major >= 6: delta = 2 self.rf.setPointSize( QFontInfo(QApplication.font()).pointSize() + delta) def createEditor(self, parent, option, index): sb = QStyledItemDelegate.createEditor(self, parent, option, index) sb.setMinimum(0) sb.setMaximum(5) sb.setSuffix(' ' + _('stars')) return sb def displayText(self, value, locale): r = int(value) if r < 0 or r > 5: r = 0 return u'\u2605' * r def sizeHint(self, option, index): option.font = self.rf option.textElideMode = self.em return QStyledItemDelegate.sizeHint(self, option, index) def paint(self, painter, option, index): option.font = self.rf option.textElideMode = self.em return QStyledItemDelegate.paint(self, painter, option, index)
def do_paint(self, painter, option, index): text = str(index.data(Qt.DisplayRole) or '') font = QFont(option.font) font.setPointSize(QFontInfo(font).pointSize() * 1.5) font2 = QFont(font) font2.setFamily(text) system, has_latin = writing_system_for_font(font2) if has_latin: font = font2 r = option.rect if option.state & QStyle.State_Selected: painter.setPen(QPen(option.palette.highlightedText(), 0)) if (option.direction == Qt.RightToLeft): r.setRight(r.right() - 4) else: r.setLeft(r.left() + 4) painter.setFont(font) painter.drawText(r, Qt.AlignVCenter|Qt.AlignLeading|Qt.TextSingleLine, text) if (system != QFontDatabase.Any): w = painter.fontMetrics().width(text + " ") painter.setFont(font2) sample = QFontDatabase().writingSystemSample(system) if (option.direction == Qt.RightToLeft): r.setRight(r.right() - w) else: r.setLeft(r.left() + w) painter.drawText(r, Qt.AlignVCenter|Qt.AlignLeading|Qt.TextSingleLine, sample)
class BlockingBusy(QDialog): def __init__(self, msg, parent=None, window_title=_('Working')): QDialog.__init__(self, parent) self._layout = QVBoxLayout() self.setLayout(self._layout) self.msg = QLabel(msg) # self.msg.setWordWrap(True) self.font = QFont() self.font.setPointSize(self.font.pointSize() + 8) self.msg.setFont(self.font) self.pi = ProgressIndicator(self) self.pi.setDisplaySize(100) self._layout.addWidget(self.pi, 0, Qt.AlignHCenter) self._layout.addSpacing(15) self._layout.addWidget(self.msg, 0, Qt.AlignHCenter) self.start() self.setWindowTitle(window_title) self.resize(self.sizeHint()) def start(self): self.pi.startAnimation() def stop(self): self.pi.stopAnimation() def accept(self): self.stop() return QDialog.accept(self) def reject(self): pass # Cannot cancel this dialog
def __init__(self): QWidget.__init__(self) self.layout_1 = QVBoxLayout() self.setLayout(self.layout_1) self.layout_1.setSpacing(0) self.layout_1.setContentsMargins(QMargins(0, 0, 0, 0)) self.paths_groupbox = QGroupBox('Preferences') self.layout_1.addWidget(self.paths_groupbox) self.paths_layout = QGridLayout() self.paths_groupbox.setLayout(self.paths_layout) font = QFont() font.setBold(False) font.setPointSize(10) self.label1 = QLabel() self.label1.setTextFormat(1) self.label1.setText( "<center><font color='#0404B4'> Please Customize Directly Within Library Codes </font></center>" ) self.label1.setFont(font) self.paths_layout.addWidget(self.label1) self.resize(self.sizeHint())
def do_paint(self, painter, option, index): text = unicode(index.data(Qt.DisplayRole) or '') font = QFont(option.font) font.setPointSize(QFontInfo(font).pointSize() * 1.5) font2 = QFont(font) font2.setFamily(text) system, has_latin = writing_system_for_font(font2) if has_latin: font = font2 r = option.rect if option.state & QStyle.State_Selected: painter.setPen(QPen(option.palette.highlightedText(), 0)) if (option.direction == Qt.RightToLeft): r.setRight(r.right() - 4) else: r.setLeft(r.left() + 4) painter.setFont(font) painter.drawText(r, Qt.AlignVCenter|Qt.AlignLeading|Qt.TextSingleLine, text) if (system != QFontDatabase.Any): w = painter.fontMetrics().width(text + " ") painter.setFont(font2) sample = QFontDatabase().writingSystemSample(system) if (option.direction == Qt.RightToLeft): r.setRight(r.right() - w) else: r.setLeft(r.left() + w) painter.drawText(r, Qt.AlignVCenter|Qt.AlignLeading|Qt.TextSingleLine, sample)
def __init__ (self,parent=None): super(Title,self).__init__(parent) self.setAlignment(QtCore.Qt.AlignCenter) font = QFont() #font.setFamily("Nyala") font.setFamily("Old English Text MT") font.setPointSize(24) self.setFont(font)
def data(self, index, role=None): try: row = index.row() column = index.column() if self.mode == 'Audio': if role == Qt.DisplayRole: if column == 0: return str(row + 1) elif column == 1: return self.playlist[row][0].split('/')[-1].split( '.')[0] elif column == 2: return str( datetime.timedelta( minutes=int(self.playlist[row][1])))[:-3] elif role == Qt.TextAlignmentRole: if column == 1: return Qt.AlignVCenter | Qt.AlignLeft elif column == 0: return Qt.AlignVCenter | Qt.AlignCenter elif role == Qt.TextColorRole: if row == self.activeInd: return QBrush(QColor(222, 142, 55)) if row in self.changed: return QBrush(QColor(222, 142, 55)) else: return QBrush(QColor(255, 255, 255)) elif role == Qt.BackgroundColorRole: if row in self.changed: return QBrush(QColor(200, 220, 200)) elif role == Qt.FontRole: f = QFont() f.setPointSize(11) f.setWeight(QFont.Bold) return f elif self.mode == 'Radio' or self.mode == 'Video': if role == Qt.DisplayRole: if column == 0: return str(row + 1) elif column == 1: return self.playlist[row][0]['name'] elif role == Qt.TextAlignmentRole: if column == 1: return Qt.AlignVCenter | Qt.AlignLeft elif column == 0: return Qt.AlignVCenter | Qt.AlignCenter elif role == Qt.TextColorRole: if row == self.activeInd: return QBrush(QColor(222, 142, 55)) else: return QBrush(QColor(255, 255, 255)) elif role == Qt.FontRole: f = QFont() f.setPointSize(11) f.setWeight(QFont.Bold) return f except Exception as e: print('Ошибка модели:', e)
def __init__(self, args, force_calibre_style=False, override_program_name=None, headless=False, color_prefs=gprefs): self.file_event_hook = None if override_program_name: args = [override_program_name] + args[1:] if headless: if not args: args = sys.argv[:1] args.extend(['-platformpluginpath', sys.extensions_location, '-platform', 'headless']) qargs = [i.encode('utf-8') if isinstance(i, unicode) else i for i in args] self.pi = plugins['progress_indicator'][0] QApplication.__init__(self, qargs) self.setup_styles(force_calibre_style) f = QFont(QApplication.font()) if (f.family(), f.pointSize()) == ('Sans Serif', 9): # Hard coded Qt settings, no user preference detected f.setPointSize(10) QApplication.setFont(f) f = QFontInfo(f) self.original_font = (f.family(), f.pointSize(), f.weight(), f.italic(), 100) if not self.using_calibre_style and self.style().objectName() == 'fusion': # Since Qt is using the fusion style anyway, specialize it self.load_calibre_style() fi = gprefs['font'] if fi is not None: font = QFont(*(fi[:4])) s = gprefs.get('font_stretch', None) if s is not None: font.setStretch(s) QApplication.setFont(font) dl = QLocale(get_lang()) if unicode(dl.bcp47Name()) != u'C': QLocale.setDefault(dl) global gui_thread, qt_app gui_thread = QThread.currentThread() self._translator = None self.load_translations() qt_app = self self._file_open_paths = [] self._file_open_lock = RLock() if not isosx: # OS X uses a native color dialog that does not support custom # colors self.color_prefs = color_prefs self.read_custom_colors() self.lastWindowClosed.connect(self.save_custom_colors) if isxp: error_dialog(None, _('Windows XP not supported'), '<p>' + _( 'calibre versions newer than 2.0 do not run on Windows XP. This is' ' because the graphics toolkit calibre uses (Qt 5) crashes a lot' ' on Windows XP. We suggest you stay with <a href="%s">calibre 1.48</a>' ' which works well on Windows XP.') % 'http://download.calibre-ebook.com/1.48.0/', show=True) raise SystemExit(1)
def __init__(self, parent, icon_name, title): QHBoxLayout.__init__(self) self.title_image_label = QLabel(parent) self.update_title_icon(icon_name) self.addWidget(self.title_image_label) title_font = QFont() title_font.setPointSize(16) shelf_label = QLabel(title, parent) shelf_label.setFont(title_font) self.addWidget(shelf_label) self.insertStretch(-1)
def __init__(self, args, force_calibre_style=False, override_program_name=None, headless=False): self.file_event_hook = None if override_program_name: args = [override_program_name] + args[1:] if headless: if not args: args = sys.argv[:1] args.extend([ '-platformpluginpath', sys.extensions_location, '-platform', 'headless' ]) qargs = [ i.encode('utf-8') if isinstance(i, unicode) else i for i in args ] self.pi = plugins['progress_indicator'][0] self.setup_styles(force_calibre_style) QApplication.__init__(self, qargs) f = QFont(QApplication.font()) if (f.family(), f.pointSize()) == ( 'Sans Serif', 9): # Hard coded Qt settings, no user preference detected f.setPointSize(10) QApplication.setFont(f) f = QFontInfo(f) self.original_font = (f.family(), f.pointSize(), f.weight(), f.italic(), 100) if not self.using_calibre_style and self.style().objectName( ) == 'fusion': # Since Qt is using the fusion style anyway, specialize it self.load_calibre_style() fi = gprefs['font'] if fi is not None: font = QFont(*(fi[:4])) s = gprefs.get('font_stretch', None) if s is not None: font.setStretch(s) QApplication.setFont(font) dl = QLocale(get_lang()) if unicode(dl.bcp47Name()) != u'C': QLocale.setDefault(dl) global gui_thread, qt_app gui_thread = QThread.currentThread() self._translator = None self.load_translations() qt_app = self self._file_open_paths = [] self._file_open_lock = RLock()
class RatingDelegate(QStyledItemDelegate, UpdateEditorGeometry): # {{{ def __init__(self, *args, **kwargs): QStyledItemDelegate.__init__(self, *args, **kwargs) self.table_widget = args[0] self.rf = QFont(rating_font()) self.em = Qt.ElideMiddle delta = 0 if iswindows and sys.getwindowsversion().major >= 6: delta = 2 self.rf.setPointSize(QFontInfo(QApplication.font()).pointSize()+delta) def createEditor(self, parent, option, index): sb = QSpinBox(parent) sb.setMinimum(0) sb.setMaximum(5) sb.setSuffix(' ' + _('stars')) sb.setSpecialValueText(_('Not rated')) return sb def get_required_width(self, editor, style, fm): val = editor.maximum() text = editor.textFromValue(val) + editor.suffix() srect = style.itemTextRect(fm, editor.geometry(), Qt.AlignLeft, False, text + u'M') return srect.width() def displayText(self, value, locale): r = int(value) if r < 0 or r > 5: r = 0 return u'\u2605'*r def setEditorData(self, editor, index): if check_key_modifier(Qt.ControlModifier): val = 0 else: val = index.data(Qt.EditRole) editor.setValue(val) def sizeHint(self, option, index): option.font = self.rf option.textElideMode = self.em return QStyledItemDelegate.sizeHint(self, option, index) def paint(self, painter, option, index): option.font = self.rf option.textElideMode = self.em return QStyledItemDelegate.paint(self, painter, option, index)
class RatingDelegate(QStyledItemDelegate, UpdateEditorGeometry): # {{{ def __init__(self, *args, **kwargs): QStyledItemDelegate.__init__(self, *args, **kwargs) self.table_widget = args[0] self.rf = QFont(rating_font()) self.em = Qt.ElideMiddle delta = 0 if iswindows and sys.getwindowsversion().major >= 6: delta = 2 self.rf.setPointSize( QFontInfo(QApplication.font()).pointSize() + delta) def createEditor(self, parent, option, index): sb = QSpinBox(parent) sb.setMinimum(0) sb.setMaximum(5) sb.setSuffix(' ' + _('stars')) sb.setSpecialValueText(_('Not rated')) return sb def get_required_width(self, editor, style, fm): val = editor.maximum() text = editor.textFromValue(val) + editor.suffix() srect = style.itemTextRect(fm, editor.geometry(), Qt.AlignLeft, False, text + u'M') return srect.width() def displayText(self, value, locale): r = int(value) if r < 0 or r > 5: r = 0 return u'\u2605' * r def setEditorData(self, editor, index): if check_key_modifier(Qt.ControlModifier): val = 0 else: val = index.data(Qt.EditRole) editor.setValue(val) def sizeHint(self, option, index): option.font = self.rf option.textElideMode = self.em return QStyledItemDelegate.sizeHint(self, option, index) def paint(self, painter, option, index): option.font = self.rf option.textElideMode = self.em return QStyledItemDelegate.paint(self, painter, option, index)
def set_button_style(self, attr_value_dict: dict): """ :param attr_value_dict: Available styles: 'color' - text color 'background' 'font' - font family 'font_size' 'bold' 'italic' 'underline' 'autoraise' - autoraise button 'on_remove' - handler of remove function, None if no need Example: button.set_button_style({'color':'magenta' , 'font_size': 18}) """ if not attr_value_dict: return style = '' if 'color' in attr_value_dict and attr_value_dict['color']: style += 'color: ' + attr_value_dict['color'] + ';' if 'background' in attr_value_dict: style += 'background-color: ' + attr_value_dict['background'] + ';' if style is not '': self.setStyleSheet('QToolButton {' + style + '}') font = QFont() if 'font' in attr_value_dict: font.setFamily(attr_value_dict['font']) if 'font_size' in attr_value_dict: font.setPointSize(attr_value_dict['font_size']) font.setBold(attr_value_dict.get('bold', False)) font.setItalic(attr_value_dict.get('italic', False)) font.setUnderline(attr_value_dict.get('underline', False)) self.setFont(font) self.setAutoRaise(attr_value_dict.get('autoraise', True)) self.remove = attr_value_dict.get('on_remove', None) if self.remove: # set button context menu policy self.setContextMenuPolicy(Qt.CustomContextMenu) self.customContextMenuRequested.connect(self.on_context_menu) # create context menu self.popMenu = QMenu(self) menu_action = QAction('Remove', self, triggered=self.remove) self.popMenu.addAction(menu_action)
def setup_ui_font(self): f = QFont(QApplication.font()) q = (f.family(), f.pointSize()) if iswindows: if q == ('MS Shell Dlg 2', 8): # Qt default setting # Microsoft recommends the default font be Segoe UI at 9 pt # https://msdn.microsoft.com/en-us/library/windows/desktop/dn742483(v=vs.85).aspx f.setFamily('Segoe UI') f.setPointSize(9) QApplication.setFont(f) else: if q == ('Sans Serif', 9): # Hard coded Qt settings, no user preference detected f.setPointSize(10) QApplication.setFont(f) f = QFontInfo(f) self.original_font = (f.family(), f.pointSize(), f.weight(), f.italic(), 100)
def __init__(self, parent, icon_name, title): ''' :param parent: Parent gui :param icon_name: Path to plugin image resource :param title: String to be displayed beside the image ''' QHBoxLayout.__init__(self) self.title_image_label = QLabel(parent) self.update_title_icon(icon_name) self.addWidget(self.title_image_label) title_font = QFont() title_font.setPointSize(16) shelf_label = QLabel(title, parent) shelf_label.setFont(title_font) self.addWidget(shelf_label) self.insertStretch(-1)
def init_ui(self): self.setWindowTitle('Программа лояльности клиентов') main_layout = QVBoxLayout() # create own font font = QFont(None) font.setPointSize(14) # create other layouts input_layout = QHBoxLayout() buttons_layout = QVBoxLayout() # create other widgets self.customer_card_input = QLineEdit() label = QLabel('Введите номер дисконтной карты: ') show_customers = QPushButton('Вывести записи о покупателе') show_discounts = QPushButton('Показать скидку для покупателей') # set font for widgets label.setFont(font) show_customers.setFont(font) show_discounts.setFont(font) self.customer_card_input.setFont(font) # connect buttons to functions show_customers.clicked.connect(self.show_customers) show_discounts.clicked.connect(self.show_discounts) # create a table and set it up self.table = QTableWidget() self.table.setFont(font) # set layouts up input_layout.addWidget(label) input_layout.addWidget(self.customer_card_input) buttons_layout.addLayout(input_layout) buttons_layout.addWidget(show_customers) buttons_layout.addWidget(show_discounts) main_layout.addLayout(buttons_layout) main_layout.addWidget(self.table) self.load_table() self.setLayout(main_layout)
def __initView(self): self.setFixedSize(600, 400) self.setWindowTitle('Application') main_layout = QHBoxLayout(self) main_layout.setSpacing(10) main_layout.addWidget(self.__paintBoard) sub_layout = QVBoxLayout() sub_layout.setContentsMargins(10, 10, 10, 10) sub_layout.setSpacing(30) self.__btn_Clear = QPushButton('clear') self.__btn_Clear.setParent(self) self.__btn_Clear.clicked.connect(self.__paintBoard.clear) sub_layout.addWidget(self.__btn_Clear) self.__btn_Predict = QPushButton('predict') self.__btn_Predict.setParent(self) self.__btn_Predict.clicked.connect(self.predict) sub_layout.addWidget(self.__btn_Predict) self.__btn_Quit = QPushButton('quit') self.__btn_Quit.setParent(self) self.__btn_Quit.clicked.connect(self.quit) sub_layout.addWidget(self.__btn_Quit) self.__lb_Result_Tip = QLabel() font = QFont() font.setPointSize(24) self.__lb_Result_Tip.setFont(font) self.__lb_Result_Tip.setText('result') self.__lb_Result_Tip.setParent(self) sub_layout.addWidget(self.__lb_Result_Tip) self.__lb_Result = QLabel() font = QFont() font.setPointSize(30) self.__lb_Result.setFont(font) self.__lb_Result.setParent(self) self.__lb_Result.setAlignment(Qt.AlignHCenter) sub_layout.addWidget(self.__lb_Result) main_layout.addLayout(sub_layout)
def __init__(self, parent, icon_name, title): super(ImageTitleLayout, self).__init__() self.title_image_label = QLabel(parent) self.update_title_icon(icon_name) self.addWidget(self.title_image_label) title_font = QFont() title_font.setPointSize(16) shelf_label = QLabel(title, parent) shelf_label.setFont(title_font) self.addWidget(shelf_label) # Add hyperlink to a help file at the right. We will replace the correct name when it is clicked. help_label = QLabel(('<a href="http://www.foo.com/">{0}</a>').format(_("Help")), parent) help_label.setTextInteractionFlags(Qt.LinksAccessibleByMouse | Qt.LinksAccessibleByKeyboard) help_label.setAlignment(Qt.AlignRight) help_label.linkActivated.connect(parent.help_link_activated) self.addWidget(help_label)
def inset_list_front(self, message, now_time, **keywords): msg = now_time + " " if 'ordernum' in keywords: pass msg += "订单 " + str(keywords.get('ordernum')) + " " if 'locationid' in keywords: pass msg += "库位【 " + str(keywords.get('locationid')) + " 】 " msg += message self.PackForm.scan_info_lv.insertItem(0, msg) font = QFont() font.setPointSize(13) self.PackForm.scan_info_lv.item(0).setBackground(QColor('yellow')) self.PackForm.scan_info_lv.item(0).setFont(font) if self.PackForm.scan_info_lv.count() > 1: self.PackForm.scan_info_lv.item(1).setBackground(QColor('white')) # 清除界面累计数据 if self.PackForm.scan_info_lv.count() > 50: self.PackForm.scan_info_lv.takeItem(50)
def __init__(self, parent, icon_name, title): QHBoxLayout.__init__(self) self.title_image_label = QLabel(parent) self.update_title_icon(icon_name) self.addWidget(self.title_image_label) title_font = QFont() title_font.setPointSize(16) shelf_label = QLabel(title, parent) shelf_label.setFont(title_font) self.addWidget(shelf_label) self.insertStretch(-1) # Add hyperlink to a help file at the right. We will replace the correct name when it is clicked. help_label = QLabel(('<a href="http://www.foo.com/">{0}</a>').format(_("Help")), parent) help_label.setTextInteractionFlags(Qt.LinksAccessibleByMouse | Qt.LinksAccessibleByKeyboard) help_label.setAlignment(Qt.AlignRight) help_label.linkActivated.connect(parent.help_link_activated) self.addWidget(help_label)
def __init__(self, parent, icon_name, title): QHBoxLayout.__init__(self) title_font = QFont() title_font.setPointSize(16) title_image_label = QLabel(parent) pixmap = QPixmap() pixmap.load(I(icon_name)) if pixmap is None: error_dialog(parent, _('Restart required'), _('You must restart calibre before using this plugin!'), show=True) else: title_image_label.setPixmap(pixmap) title_image_label.setMaximumSize(32, 32) title_image_label.setScaledContents(True) self.addWidget(title_image_label) shelf_label = QLabel(title, parent) shelf_label.setFont(title_font) self.addWidget(shelf_label) self.insertStretch(-1)
class RatingDelegate(QStyledItemDelegate, UpdateEditorGeometry): # {{{ def __init__(self, *args, **kwargs): QStyledItemDelegate.__init__(self, *args) self.is_half_star = kwargs.get('is_half_star', False) self.table_widget = args[0] self.rf = QFont(rating_font()) self.em = Qt.ElideMiddle delta = 0 if iswindows and sys.getwindowsversion().major >= 6: delta = 2 self.rf.setPointSize( QFontInfo(QApplication.font()).pointSize() + delta) def get_required_width(self, editor, style, fm): return editor.sizeHint().width() def displayText(self, value, locale): return rating_to_stars(value, self.is_half_star) def createEditor(self, parent, option, index): return RatingEditor(parent, is_half_star=self.is_half_star) def setEditorData(self, editor, index): if check_key_modifier(Qt.ControlModifier): val = 0 else: val = index.data(Qt.EditRole) editor.rating_value = val def setModelData(self, editor, model, index): val = editor.rating_value model.setData(index, val, Qt.EditRole) def sizeHint(self, option, index): option.font = self.rf option.textElideMode = self.em return QStyledItemDelegate.sizeHint(self, option, index) def paint(self, painter, option, index): option.font = self.rf option.textElideMode = self.em return QStyledItemDelegate.paint(self, painter, option, index)
class RatingDelegate(QStyledItemDelegate, UpdateEditorGeometry): # {{{ def __init__(self, *args, **kwargs): QStyledItemDelegate.__init__(self, *args, **kwargs) self.is_half_star = kwargs.get('is_half_star', False) self.table_widget = args[0] self.rf = QFont(rating_font()) self.em = Qt.ElideMiddle delta = 0 if iswindows and sys.getwindowsversion().major >= 6: delta = 2 self.rf.setPointSize(QFontInfo(QApplication.font()).pointSize()+delta) def get_required_width(self, editor, style, fm): return editor.sizeHint().width() def displayText(self, value, locale): return rating_to_stars(value, self.is_half_star) def createEditor(self, parent, option, index): return RatingEditor(parent, is_half_star=self.is_half_star) def setEditorData(self, editor, index): if check_key_modifier(Qt.ControlModifier): val = 0 else: val = index.data(Qt.EditRole) editor.rating_value = val def setModelData(self, editor, model, index): val = editor.rating_value model.setData(index, val, Qt.EditRole) def sizeHint(self, option, index): option.font = self.rf option.textElideMode = self.em return QStyledItemDelegate.sizeHint(self, option, index) def paint(self, painter, option, index): option.font = self.rf option.textElideMode = self.em return QStyledItemDelegate.paint(self, painter, option, index)
def __init__(self, args, force_calibre_style=False, override_program_name=None, headless=False): self.file_event_hook = None if override_program_name: args = [override_program_name] + args[1:] if headless: if not args: args = sys.argv[:1] args.extend(['-platformpluginpath', sys.extensions_location, '-platform', 'headless']) qargs = [i.encode('utf-8') if isinstance(i, unicode) else i for i in args] self.pi = plugins['progress_indicator'][0] self.setup_styles(force_calibre_style) QApplication.__init__(self, qargs) f = QFont(QApplication.font()) if (f.family(), f.pointSize()) == ('Sans Serif', 9): # Hard coded Qt settings, no user preference detected f.setPointSize(10) QApplication.setFont(f) f = QFontInfo(f) self.original_font = (f.family(), f.pointSize(), f.weight(), f.italic(), 100) if not self.using_calibre_style and self.style().objectName() == 'fusion': # Since Qt is using the fusion style anyway, specialize it self.load_calibre_style() fi = gprefs['font'] if fi is not None: font = QFont(*(fi[:4])) s = gprefs.get('font_stretch', None) if s is not None: font.setStretch(s) QApplication.setFont(font) dl = QLocale(get_lang()) if unicode(dl.bcp47Name()) != u'C': QLocale.setDefault(dl) global gui_thread, qt_app gui_thread = QThread.currentThread() self._translator = None self.load_translations() qt_app = self self._file_open_paths = [] self._file_open_lock = RLock()
def generate_buttons(self): self.buttons = [] self.GameName.setText(str(self.N)+'-Puzzle') width = int(m.sqrt(self.N+1)) for i in range(width): for j in range(width): button = QtWidgets.QPushButton(self.widget) button.setMaximumHeight(120) button.setMaximumWidth(120) button.setObjectName(str(i*width+j)) font = QFont() font.setPointSize(14) font.setBold(1) button.setFont(font) button.setText(str(state[i*width+j])) button.clicked.connect(lambda x:self.push_button(int(self.sender().objectName()))) self.buttons.append(button) self.ButtonLayout.addWidget(button, i, j)
def __init__(self, parent, icon_name, title, tooltip=None): QHBoxLayout.__init__(self) title_image_label = QLabel(parent) pixmap = get_pixmap(icon_name) if pixmap is None: pixmap = get_pixmap('library.png') # error_dialog(parent, _('Restart required'), # _('You must restart Calibre before using this plugin!'), show=True) else: title_image_label.setPixmap(pixmap) title_image_label.setMaximumSize(32, 32) title_image_label.setScaledContents(True) self.addWidget(title_image_label) title_font = QFont() title_font.setPointSize(16) shelf_label = QLabel(title, parent) shelf_label.setFont(title_font) self.addWidget(shelf_label) self.insertStretch(-1) if tooltip: title_image_label.setToolTip(tooltip) shelf_label.setToolTip(tooltip)
class TextEdit(PlainTextEdit): link_clicked = pyqtSignal(object) smart_highlighting_updated = pyqtSignal() def __init__(self, parent=None, expected_geometry=(100, 50)): PlainTextEdit.__init__(self, parent) self.snippet_manager = SnippetManager(self) self.completion_popup = CompletionPopup(self) self.request_completion = self.completion_doc_name = None self.clear_completion_cache_timer = t = QTimer(self) t.setInterval(5000), t.timeout.connect(self.clear_completion_cache), t.setSingleShot(True) self.textChanged.connect(t.start) self.last_completion_request = -1 self.gutter_width = 0 self.tw = 2 self.expected_geometry = expected_geometry self.saved_matches = {} self.syntax = None self.smarts = NullSmarts(self) self.current_cursor_line = None self.current_search_mark = None self.smarts_highlight_timer = t = QTimer() t.setInterval(750), t.setSingleShot(True), t.timeout.connect(self.update_extra_selections) self.highlighter = SyntaxHighlighter() self.line_number_area = LineNumbers(self) self.apply_settings() self.setMouseTracking(True) self.cursorPositionChanged.connect(self.highlight_cursor_line) self.blockCountChanged[int].connect(self.update_line_number_area_width) self.updateRequest.connect(self.update_line_number_area) def get_droppable_files(self, md): def is_mt_ok(mt): return self.syntax == 'html' and ( mt in OEB_DOCS or mt in OEB_STYLES or mt.startswith('image/') ) if md.hasFormat(CONTAINER_DND_MIMETYPE): for line in as_unicode(bytes(md.data(CONTAINER_DND_MIMETYPE))).splitlines(): mt = current_container().mime_map.get(line, 'application/octet-stream') if is_mt_ok(mt): yield line, mt, True return for qurl in md.urls(): if qurl.isLocalFile() and os.access(qurl.toLocalFile(), os.R_OK): path = qurl.toLocalFile() mt = guess_type(path) if is_mt_ok(mt): yield path, mt, False def canInsertFromMimeData(self, md): if md.hasText() or (md.hasHtml() and self.syntax == 'html') or md.hasImage(): return True elif tuple(self.get_droppable_files(md)): return True return False def insertFromMimeData(self, md): files = tuple(self.get_droppable_files(md)) base = self.highlighter.doc_name or None def get_name(name): folder = get_recommended_folders(current_container(), (name,))[name] or '' if folder: folder += '/' return folder + name def get_href(name): return current_container().name_to_href(name, base) def insert_text(text): c = self.textCursor() c.insertText(text) self.setTextCursor(c) self.ensureCursorVisible() def add_file(name, data, mt=None): from calibre.gui2.tweak_book.boss import get_boss name = current_container().add_file(name, data, media_type=mt, modify_name_if_needed=True) get_boss().refresh_file_list() return name if files: for path, mt, is_name in files: if is_name: name = path else: name = get_name(os.path.basename(path)) with lopen(path, 'rb') as f: name = add_file(name, f.read(), mt) href = get_href(name) if mt.startswith('image/'): self.insert_image(href) elif mt in OEB_STYLES: insert_text('<link href="{}" rel="stylesheet" type="text/css"/>'.format(href)) elif mt in OEB_DOCS: self.insert_hyperlink(href, name) self.ensureCursorVisible() return if md.hasImage(): img = md.imageData() if img is not None and not img.isNull(): data = image_to_data(img, fmt='PNG') name = add_file(get_name('dropped_image.png'), data) self.insert_image(get_href(name)) self.ensureCursorVisible() return if md.hasText(): return insert_text(md.text()) if md.hasHtml(): insert_text(md.html()) return @property def is_modified(self): ''' True if the document has been modified since it was loaded or since the last time is_modified was set to False. ''' return self.document().isModified() @is_modified.setter def is_modified(self, val): self.document().setModified(bool(val)) def sizeHint(self): return self.size_hint def apply_settings(self, prefs=None, dictionaries_changed=False): # {{{ prefs = prefs or tprefs self.setAcceptDrops(prefs.get('editor_accepts_drops', True)) self.setLineWrapMode(QPlainTextEdit.WidgetWidth if prefs['editor_line_wrap'] else QPlainTextEdit.NoWrap) theme = get_theme(prefs['editor_theme']) self.apply_theme(theme) w = self.fontMetrics() self.space_width = w.width(' ') self.tw = self.smarts.override_tab_stop_width if self.smarts.override_tab_stop_width is not None else prefs['editor_tab_stop_width'] self.setTabStopWidth(self.tw * self.space_width) if dictionaries_changed: self.highlighter.rehighlight() def apply_theme(self, theme): self.theme = theme pal = self.palette() pal.setColor(pal.Base, theme_color(theme, 'Normal', 'bg')) pal.setColor(pal.AlternateBase, theme_color(theme, 'CursorLine', 'bg')) pal.setColor(pal.Text, theme_color(theme, 'Normal', 'fg')) pal.setColor(pal.Highlight, theme_color(theme, 'Visual', 'bg')) pal.setColor(pal.HighlightedText, theme_color(theme, 'Visual', 'fg')) self.setPalette(pal) self.tooltip_palette = pal = QPalette() pal.setColor(pal.ToolTipBase, theme_color(theme, 'Tooltip', 'bg')) pal.setColor(pal.ToolTipText, theme_color(theme, 'Tooltip', 'fg')) self.line_number_palette = pal = QPalette() pal.setColor(pal.Base, theme_color(theme, 'LineNr', 'bg')) pal.setColor(pal.Text, theme_color(theme, 'LineNr', 'fg')) pal.setColor(pal.BrightText, theme_color(theme, 'LineNrC', 'fg')) self.match_paren_format = theme_format(theme, 'MatchParen') font = self.font() ff = tprefs['editor_font_family'] if ff is None: ff = default_font_family() font.setFamily(ff) font.setPointSize(tprefs['editor_font_size']) self.tooltip_font = QFont(font) self.tooltip_font.setPointSize(font.pointSize() - 1) self.setFont(font) self.highlighter.apply_theme(theme) w = self.fontMetrics() self.number_width = max(map(lambda x:w.width(unicode_type(x)), range(10))) self.size_hint = QSize(self.expected_geometry[0] * w.averageCharWidth(), self.expected_geometry[1] * w.height()) self.highlight_color = theme_color(theme, 'HighlightRegion', 'bg') self.highlight_cursor_line() self.completion_popup.clear_caches(), self.completion_popup.update() # }}} def load_text(self, text, syntax='html', process_template=False, doc_name=None): self.syntax = syntax self.highlighter = get_highlighter(syntax)() self.highlighter.apply_theme(self.theme) self.highlighter.set_document(self.document(), doc_name=doc_name) sclass = get_smarts(syntax) if sclass is not None: self.smarts = sclass(self) if self.smarts.override_tab_stop_width is not None: self.tw = self.smarts.override_tab_stop_width self.setTabStopWidth(self.tw * self.space_width) self.setPlainText(unicodedata.normalize('NFC', unicode_type(text))) if process_template and QPlainTextEdit.find(self, '%CURSOR%'): c = self.textCursor() c.insertText('') def change_document_name(self, newname): self.highlighter.doc_name = newname self.highlighter.rehighlight() # Ensure links are checked w.r.t. the new name correctly def replace_text(self, text): c = self.textCursor() pos = c.position() c.beginEditBlock() c.clearSelection() c.select(c.Document) c.insertText(unicodedata.normalize('NFC', text)) c.endEditBlock() c.setPosition(min(pos, len(text))) self.setTextCursor(c) self.ensureCursorVisible() def simple_replace(self, text, cursor=None): c = cursor or self.textCursor() c.insertText(unicodedata.normalize('NFC', text)) self.setTextCursor(c) def go_to_line(self, lnum, col=None): lnum = max(1, min(self.blockCount(), lnum)) c = self.textCursor() c.clearSelection() c.movePosition(c.Start) c.movePosition(c.NextBlock, n=lnum - 1) c.movePosition(c.StartOfLine) c.movePosition(c.EndOfLine, c.KeepAnchor) text = unicode_type(c.selectedText()).rstrip('\0') if col is None: c.movePosition(c.StartOfLine) lt = text.lstrip() if text and lt and lt != text: c.movePosition(c.NextWord) else: c.setPosition(c.block().position() + col) if c.blockNumber() + 1 > lnum: # We have moved past the end of the line c.setPosition(c.block().position()) c.movePosition(c.EndOfBlock) self.setTextCursor(c) self.ensureCursorVisible() def update_extra_selections(self, instant=True): sel = [] if self.current_cursor_line is not None: sel.append(self.current_cursor_line) if self.current_search_mark is not None: sel.append(self.current_search_mark) if instant and not self.highlighter.has_requests and self.smarts is not None: sel.extend(self.smarts.get_extra_selections(self)) self.smart_highlighting_updated.emit() else: self.smarts_highlight_timer.start() self.setExtraSelections(sel) # Search and replace {{{ def mark_selected_text(self): sel = QTextEdit.ExtraSelection() sel.format.setBackground(self.highlight_color) sel.cursor = self.textCursor() if sel.cursor.hasSelection(): self.current_search_mark = sel c = self.textCursor() c.clearSelection() self.setTextCursor(c) else: self.current_search_mark = None self.update_extra_selections() def find_in_marked(self, pat, wrap=False, save_match=None): if self.current_search_mark is None: return False csm = self.current_search_mark.cursor reverse = pat.flags & regex.REVERSE c = self.textCursor() c.clearSelection() m_start = min(csm.position(), csm.anchor()) m_end = max(csm.position(), csm.anchor()) if c.position() < m_start: c.setPosition(m_start) if c.position() > m_end: c.setPosition(m_end) pos = m_start if reverse else m_end if wrap: pos = m_end if reverse else m_start c.setPosition(pos, c.KeepAnchor) raw = unicode_type(c.selectedText()).replace(PARAGRAPH_SEPARATOR, '\n').rstrip('\0') m = pat.search(raw) if m is None: return False start, end = m.span() if start == end: return False if wrap: if reverse: textpos = c.anchor() start, end = textpos + end, textpos + start else: start, end = m_start + start, m_start + end else: if reverse: start, end = m_start + end, m_start + start else: start, end = c.anchor() + start, c.anchor() + end c.clearSelection() c.setPosition(start) c.setPosition(end, c.KeepAnchor) self.setTextCursor(c) # Center search result on screen self.centerCursor() if save_match is not None: self.saved_matches[save_match] = (pat, m) return True def all_in_marked(self, pat, template=None): if self.current_search_mark is None: return 0 c = self.current_search_mark.cursor raw = unicode_type(c.selectedText()).replace(PARAGRAPH_SEPARATOR, '\n').rstrip('\0') if template is None: count = len(pat.findall(raw)) else: from calibre.gui2.tweak_book.function_replace import Function repl_is_func = isinstance(template, Function) if repl_is_func: template.init_env() raw, count = pat.subn(template, raw) if repl_is_func: from calibre.gui2.tweak_book.search import show_function_debug_output if getattr(template.func, 'append_final_output_to_marked', False): retval = template.end() if retval: raw += unicode_type(retval) else: template.end() show_function_debug_output(template) if count > 0: start_pos = min(c.anchor(), c.position()) c.insertText(raw) end_pos = max(c.anchor(), c.position()) c.setPosition(start_pos), c.setPosition(end_pos, c.KeepAnchor) self.update_extra_selections() return count def smart_comment(self): from calibre.gui2.tweak_book.editor.comments import smart_comment smart_comment(self, self.syntax) def sort_css(self): from calibre.gui2.dialogs.confirm_delete import confirm if confirm(_('Sorting CSS rules can in rare cases change the effective styles applied to the book.' ' Are you sure you want to proceed?'), 'edit-book-confirm-sort-css', parent=self, config_set=tprefs): c = self.textCursor() c.beginEditBlock() c.movePosition(c.Start), c.movePosition(c.End, c.KeepAnchor) text = unicode_type(c.selectedText()).replace(PARAGRAPH_SEPARATOR, '\n').rstrip('\0') from calibre.ebooks.oeb.polish.css import sort_sheet text = css_text(sort_sheet(current_container(), text)) c.insertText(text) c.movePosition(c.Start) c.endEditBlock() self.setTextCursor(c) def find(self, pat, wrap=False, marked=False, complete=False, save_match=None): if marked: return self.find_in_marked(pat, wrap=wrap, save_match=save_match) reverse = pat.flags & regex.REVERSE c = self.textCursor() c.clearSelection() if complete: # Search the entire text c.movePosition(c.End if reverse else c.Start) pos = c.Start if reverse else c.End if wrap and not complete: pos = c.End if reverse else c.Start c.movePosition(pos, c.KeepAnchor) raw = unicode_type(c.selectedText()).replace(PARAGRAPH_SEPARATOR, '\n').rstrip('\0') m = pat.search(raw) if m is None: return False start, end = m.span() if start == end: return False if wrap and not complete: if reverse: textpos = c.anchor() start, end = textpos + end, textpos + start else: if reverse: # Put the cursor at the start of the match start, end = end, start else: textpos = c.anchor() start, end = textpos + start, textpos + end c.clearSelection() c.setPosition(start) c.setPosition(end, c.KeepAnchor) self.setTextCursor(c) # Center search result on screen self.centerCursor() if save_match is not None: self.saved_matches[save_match] = (pat, m) return True def find_text(self, pat, wrap=False, complete=False): reverse = pat.flags & regex.REVERSE c = self.textCursor() c.clearSelection() if complete: # Search the entire text c.movePosition(c.End if reverse else c.Start) pos = c.Start if reverse else c.End if wrap and not complete: pos = c.End if reverse else c.Start c.movePosition(pos, c.KeepAnchor) if hasattr(self.smarts, 'find_text'): self.highlighter.join() found, start, end = self.smarts.find_text(pat, c) if not found: return False else: raw = unicode_type(c.selectedText()).replace(PARAGRAPH_SEPARATOR, '\n').rstrip('\0') m = pat.search(raw) if m is None: return False start, end = m.span() if start == end: return False if reverse: start, end = end, start c.clearSelection() c.setPosition(start) c.setPosition(end, c.KeepAnchor) self.setTextCursor(c) # Center search result on screen self.centerCursor() return True def find_spell_word(self, original_words, lang, from_cursor=True, center_on_cursor=True): c = self.textCursor() c.setPosition(c.position()) if not from_cursor: c.movePosition(c.Start) c.movePosition(c.End, c.KeepAnchor) def find_first_word(haystack): match_pos, match_word = -1, None for w in original_words: idx = index_of(w, haystack, lang=lang) if idx > -1 and (match_pos == -1 or match_pos > idx): match_pos, match_word = idx, w return match_pos, match_word while True: text = unicode_type(c.selectedText()).rstrip('\0') idx, word = find_first_word(text) if idx == -1: return False c.setPosition(c.anchor() + idx) c.setPosition(c.position() + string_length(word), c.KeepAnchor) if self.smarts.verify_for_spellcheck(c, self.highlighter): self.highlighter.join() # Ensure highlighting is finished locale = self.spellcheck_locale_for_cursor(c) if not lang or not locale or (locale and lang == locale.langcode): self.setTextCursor(c) if center_on_cursor: self.centerCursor() return True c.setPosition(c.position()) c.movePosition(c.End, c.KeepAnchor) return False def find_next_spell_error(self, from_cursor=True): c = self.textCursor() if not from_cursor: c.movePosition(c.Start) block = c.block() while block.isValid(): for r in block.layout().additionalFormats(): if r.format.property(SPELL_PROPERTY): if not from_cursor or block.position() + r.start + r.length > c.position(): c.setPosition(block.position() + r.start) c.setPosition(c.position() + r.length, c.KeepAnchor) self.setTextCursor(c) return True block = block.next() return False def replace(self, pat, template, saved_match='gui'): c = self.textCursor() raw = unicode_type(c.selectedText()).replace(PARAGRAPH_SEPARATOR, '\n').rstrip('\0') m = pat.fullmatch(raw) if m is None: # This can happen if either the user changed the selected text or # the search expression uses lookahead/lookbehind operators. See if # the saved match matches the currently selected text and # use it, if so. if saved_match is not None and saved_match in self.saved_matches: saved_pat, saved = self.saved_matches.pop(saved_match) if saved_pat == pat and saved.group() == raw: m = saved if m is None: return False if callable(template): text = template(m) else: text = m.expand(template) c.insertText(text) return True def go_to_anchor(self, anchor): if anchor is TOP: c = self.textCursor() c.movePosition(c.Start) self.setTextCursor(c) return True base = r'''%%s\s*=\s*['"]{0,1}%s''' % regex.escape(anchor) raw = unicode_type(self.toPlainText()) m = regex.search(base % 'id', raw) if m is None: m = regex.search(base % 'name', raw) if m is not None: c = self.textCursor() c.setPosition(m.start()) self.setTextCursor(c) return True return False # }}} # Line numbers and cursor line {{{ def highlight_cursor_line(self): sel = QTextEdit.ExtraSelection() sel.format.setBackground(self.palette().alternateBase()) sel.format.setProperty(QTextFormat.FullWidthSelection, True) sel.cursor = self.textCursor() sel.cursor.clearSelection() self.current_cursor_line = sel self.update_extra_selections(instant=False) # Update the cursor line's line number in the line number area try: self.line_number_area.update(0, self.last_current_lnum[0], self.line_number_area.width(), self.last_current_lnum[1]) except AttributeError: pass block = self.textCursor().block() top = int(self.blockBoundingGeometry(block).translated(self.contentOffset()).top()) height = int(self.blockBoundingRect(block).height()) self.line_number_area.update(0, top, self.line_number_area.width(), height) def update_line_number_area_width(self, block_count=0): self.gutter_width = self.line_number_area_width() self.setViewportMargins(self.gutter_width, 0, 0, 0) def line_number_area_width(self): digits = 1 limit = max(1, self.blockCount()) while limit >= 10: limit /= 10 digits += 1 return 8 + self.number_width * digits def update_line_number_area(self, rect, dy): if dy: self.line_number_area.scroll(0, dy) else: self.line_number_area.update(0, rect.y(), self.line_number_area.width(), rect.height()) if rect.contains(self.viewport().rect()): self.update_line_number_area_width() def resizeEvent(self, ev): QPlainTextEdit.resizeEvent(self, ev) cr = self.contentsRect() self.line_number_area.setGeometry(QRect(cr.left(), cr.top(), self.line_number_area_width(), cr.height())) def paint_line_numbers(self, ev): painter = QPainter(self.line_number_area) painter.fillRect(ev.rect(), self.line_number_palette.color(QPalette.Base)) block = self.firstVisibleBlock() num = block.blockNumber() top = int(self.blockBoundingGeometry(block).translated(self.contentOffset()).top()) bottom = top + int(self.blockBoundingRect(block).height()) current = self.textCursor().block().blockNumber() painter.setPen(self.line_number_palette.color(QPalette.Text)) while block.isValid() and top <= ev.rect().bottom(): if block.isVisible() and bottom >= ev.rect().top(): if current == num: painter.save() painter.setPen(self.line_number_palette.color(QPalette.BrightText)) f = QFont(self.font()) f.setBold(True) painter.setFont(f) self.last_current_lnum = (top, bottom - top) painter.drawText(0, top, self.line_number_area.width() - 5, self.fontMetrics().height(), Qt.AlignRight, unicode_type(num + 1)) if current == num: painter.restore() block = block.next() top = bottom bottom = top + int(self.blockBoundingRect(block).height()) num += 1 # }}} def override_shortcut(self, ev): # Let the global cut/copy/paste/undo/redo shortcuts work, this avoids the nbsp # problem as well, since they use the overridden createMimeDataFromSelection() method # instead of the one from Qt (which makes copy() work), and allows proper customization # of the shortcuts if ev in (QKeySequence.Copy, QKeySequence.Cut, QKeySequence.Paste, QKeySequence.Undo, QKeySequence.Redo): ev.ignore() return True # This is used to convert typed hex codes into unicode # characters if ev.key() == Qt.Key_X and ev.modifiers() == Qt.AltModifier: ev.accept() return True return PlainTextEdit.override_shortcut(self, ev) def text_for_range(self, block, r): c = self.textCursor() c.setPosition(block.position() + r.start) c.setPosition(c.position() + r.length, c.KeepAnchor) return unicode_type(c.selectedText()) def spellcheck_locale_for_cursor(self, c): with store_locale: formats = self.highlighter.parse_single_block(c.block())[0] pos = c.positionInBlock() for r in formats: if r.start <= pos <= r.start + r.length and r.format.property(SPELL_PROPERTY): return r.format.property(SPELL_LOCALE_PROPERTY) def recheck_word(self, word, locale): c = self.textCursor() c.movePosition(c.Start) block = c.block() while block.isValid(): for r in block.layout().additionalFormats(): if r.format.property(SPELL_PROPERTY) and self.text_for_range(block, r) == word: self.highlighter.reformat_block(block) break block = block.next() # Tooltips {{{ def syntax_range_for_cursor(self, cursor): if cursor.isNull(): return pos = cursor.positionInBlock() for r in cursor.block().layout().additionalFormats(): if r.start <= pos <= r.start + r.length and r.format.property(SYNTAX_PROPERTY): return r def syntax_format_for_cursor(self, cursor): return getattr(self.syntax_range_for_cursor(cursor), 'format', None) def show_tooltip(self, ev): c = self.cursorForPosition(ev.pos()) fmt = self.syntax_format_for_cursor(c) if fmt is not None: tt = unicode_type(fmt.toolTip()) if tt: QToolTip.setFont(self.tooltip_font) QToolTip.setPalette(self.tooltip_palette) QToolTip.showText(ev.globalPos(), textwrap.fill(tt)) return QToolTip.hideText() ev.ignore() # }}} def link_for_position(self, pos): c = self.cursorForPosition(pos) r = self.syntax_range_for_cursor(c) if r is not None and r.format.property(LINK_PROPERTY): return self.text_for_range(c.block(), r) def mousePressEvent(self, ev): if self.completion_popup.isVisible() and not self.completion_popup.rect().contains(ev.pos()): # For some reason using eventFilter for this does not work, so we # implement it here self.completion_popup.abort() if ev.modifiers() & Qt.CTRL: url = self.link_for_position(ev.pos()) if url is not None: ev.accept() self.link_clicked.emit(url) return return PlainTextEdit.mousePressEvent(self, ev) def get_range_inside_tag(self): c = self.textCursor() left = min(c.anchor(), c.position()) right = max(c.anchor(), c.position()) # For speed we use QPlainTextEdit's toPlainText as we dont care about # spaces in this context raw = unicode_type(QPlainTextEdit.toPlainText(self)) # Make sure the left edge is not within a <> gtpos = raw.find('>', left) ltpos = raw.find('<', left) if gtpos < ltpos: left = gtpos + 1 if gtpos > -1 else left right = max(left, right) if right != left: gtpos = raw.find('>', right) ltpos = raw.find('<', right) if ltpos > gtpos: ltpos = raw.rfind('<', left, right+1) right = max(ltpos, left) return left, right def format_text(self, formatting): if self.syntax != 'html': return if formatting.startswith('justify_'): return self.smarts.set_text_alignment(self, formatting.partition('_')[-1]) color = 'currentColor' if formatting in {'color', 'background-color'}: color = QColorDialog.getColor(QColor(Qt.black if formatting == 'color' else Qt.white), self, _('Choose color'), QColorDialog.ShowAlphaChannel) if not color.isValid(): return r, g, b, a = color.getRgb() if a == 255: color = 'rgb(%d, %d, %d)' % (r, g, b) else: color = 'rgba(%d, %d, %d, %.2g)' % (r, g, b, a/255) prefix, suffix = { 'bold': ('<b>', '</b>'), 'italic': ('<i>', '</i>'), 'underline': ('<u>', '</u>'), 'strikethrough': ('<strike>', '</strike>'), 'superscript': ('<sup>', '</sup>'), 'subscript': ('<sub>', '</sub>'), 'color': ('<span style="color: %s">' % color, '</span>'), 'background-color': ('<span style="background-color: %s">' % color, '</span>'), }[formatting] left, right = self.get_range_inside_tag() c = self.textCursor() c.setPosition(left) c.setPosition(right, c.KeepAnchor) prev_text = unicode_type(c.selectedText()).rstrip('\0') c.insertText(prefix + prev_text + suffix) if prev_text: right = c.position() c.setPosition(left) c.setPosition(right, c.KeepAnchor) else: c.setPosition(c.position() - len(suffix)) self.setTextCursor(c) def insert_image(self, href, fullpage=False, preserve_aspect_ratio=False, width=-1, height=-1): if width <= 0: width = 1200 if height <= 0: height = 1600 c = self.textCursor() template, alt = 'url(%s)', '' left = min(c.position(), c.anchor) if self.syntax == 'html': left, right = self.get_range_inside_tag() c.setPosition(left) c.setPosition(right, c.KeepAnchor) href = prepare_string_for_xml(href, True) if fullpage: template = '''\ <div style="page-break-before:always; page-break-after:always; page-break-inside:avoid">\ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" \ version="1.1" width="100%%" height="100%%" viewBox="0 0 {w} {h}" preserveAspectRatio="{a}">\ <image width="{w}" height="{h}" xlink:href="%s"/>\ </svg></div>'''.format(w=width, h=height, a='xMidYMid meet' if preserve_aspect_ratio else 'none') else: alt = _('Image') template = '<img alt="{0}" src="%s" />'.format(alt) text = template % href c.insertText(text) if self.syntax == 'html' and not fullpage: c.setPosition(left + 10) c.setPosition(c.position() + len(alt), c.KeepAnchor) else: c.setPosition(left) c.setPosition(left + len(text), c.KeepAnchor) self.setTextCursor(c) def insert_hyperlink(self, target, text, template=None): if hasattr(self.smarts, 'insert_hyperlink'): self.smarts.insert_hyperlink(self, target, text, template=template) def insert_tag(self, tag): if hasattr(self.smarts, 'insert_tag'): self.smarts.insert_tag(self, tag) def remove_tag(self): if hasattr(self.smarts, 'remove_tag'): self.smarts.remove_tag(self) def keyPressEvent(self, ev): if ev.key() == Qt.Key_X and ev.modifiers() == Qt.AltModifier: if self.replace_possible_unicode_sequence(): ev.accept() return if ev.key() == Qt.Key_Insert: self.setOverwriteMode(self.overwriteMode() ^ True) ev.accept() return if self.snippet_manager.handle_key_press(ev): self.completion_popup.hide() return if self.smarts.handle_key_press(ev, self): self.handle_keypress_completion(ev) return QPlainTextEdit.keyPressEvent(self, ev) self.handle_keypress_completion(ev) def handle_keypress_completion(self, ev): if self.request_completion is None: return code = ev.key() if code in ( 0, Qt.Key_unknown, Qt.Key_Shift, Qt.Key_Control, Qt.Key_Alt, Qt.Key_Meta, Qt.Key_AltGr, Qt.Key_CapsLock, Qt.Key_NumLock, Qt.Key_ScrollLock, Qt.Key_Up, Qt.Key_Down): # We ignore up/down arrow so as to not break scrolling through the # text with the arrow keys return result = self.smarts.get_completion_data(self, ev) if result is None: self.last_completion_request += 1 else: self.last_completion_request = self.request_completion(*result) self.completion_popup.mark_completion(self, None if result is None else result[-1]) def handle_completion_result(self, result): if result.request_id[0] >= self.last_completion_request: self.completion_popup.handle_result(result) def clear_completion_cache(self): if self.request_completion is not None and self.completion_doc_name: self.request_completion(None, 'file:' + self.completion_doc_name) def replace_possible_unicode_sequence(self): c = self.textCursor() has_selection = c.hasSelection() if has_selection: text = unicode_type(c.selectedText()).rstrip('\0') else: c.setPosition(c.position() - min(c.positionInBlock(), 6), c.KeepAnchor) text = unicode_type(c.selectedText()).rstrip('\0') m = re.search(r'[a-fA-F0-9]{2,6}$', text) if m is None: return False text = m.group() try: num = int(text, 16) except ValueError: return False if num > 0x10ffff or num < 1: return False end_pos = max(c.anchor(), c.position()) c.setPosition(end_pos - len(text)), c.setPosition(end_pos, c.KeepAnchor) c.insertText(safe_chr(num)) return True def select_all(self): c = self.textCursor() c.clearSelection() c.setPosition(0) c.movePosition(c.End, c.KeepAnchor) self.setTextCursor(c) def rename_block_tag(self, new_name): if hasattr(self.smarts, 'rename_block_tag'): self.smarts.rename_block_tag(self, new_name) def current_tag(self, for_position_sync=True): return self.smarts.cursor_position_with_sourceline(self.textCursor(), for_position_sync=for_position_sync) def goto_sourceline(self, sourceline, tags, attribute=None): return self.smarts.goto_sourceline(self, sourceline, tags, attribute=attribute) def get_tag_contents(self): c = self.smarts.get_inner_HTML(self) if c is not None: return self.selected_text_from_cursor(c) def goto_css_rule(self, rule_address, sourceline_address=None): from calibre.gui2.tweak_book.editor.smarts.css import find_rule block = None if self.syntax == 'css': raw = unicode_type(self.toPlainText()) line, col = find_rule(raw, rule_address) if line is not None: block = self.document().findBlockByNumber(line - 1) elif sourceline_address is not None: sourceline, tags = sourceline_address if self.goto_sourceline(sourceline, tags): c = self.textCursor() c.setPosition(c.position() + 1) self.setTextCursor(c) raw = self.get_tag_contents() line, col = find_rule(raw, rule_address) if line is not None: block = self.document().findBlockByNumber(c.blockNumber() + line - 1) if block is not None and block.isValid(): c = self.textCursor() c.setPosition(block.position() + col) self.setTextCursor(c) def change_case(self, action, cursor=None): cursor = cursor or self.textCursor() text = self.selected_text_from_cursor(cursor) text = {'lower':lower, 'upper':upper, 'capitalize':capitalize, 'title':titlecase, 'swap':swapcase}[action](text) cursor.insertText(text) self.setTextCursor(cursor)
class TextEdit(PlainTextEdit): link_clicked = pyqtSignal(object) smart_highlighting_updated = pyqtSignal() def __init__(self, parent=None, expected_geometry=(100, 50)): PlainTextEdit.__init__(self, parent) self.snippet_manager = SnippetManager(self) self.completion_popup = CompletionPopup(self) self.request_completion = self.completion_doc_name = None self.clear_completion_cache_timer = t = QTimer(self) t.setInterval(5000), t.timeout.connect(self.clear_completion_cache), t.setSingleShot(True) self.textChanged.connect(t.start) self.last_completion_request = -1 self.gutter_width = 0 self.tw = 2 self.expected_geometry = expected_geometry self.saved_matches = {} self.syntax = None self.smarts = NullSmarts(self) self.current_cursor_line = None self.current_search_mark = None self.smarts_highlight_timer = t = QTimer() t.setInterval(750), t.setSingleShot(True), t.timeout.connect(self.update_extra_selections) self.highlighter = SyntaxHighlighter() self.line_number_area = LineNumbers(self) self.apply_settings() self.setMouseTracking(True) self.cursorPositionChanged.connect(self.highlight_cursor_line) self.blockCountChanged[int].connect(self.update_line_number_area_width) self.updateRequest.connect(self.update_line_number_area) @dynamic_property def is_modified(self): """ True if the document has been modified since it was loaded or since the last time is_modified was set to False. """ def fget(self): return self.document().isModified() def fset(self, val): self.document().setModified(bool(val)) return property(fget=fget, fset=fset) def sizeHint(self): return self.size_hint def apply_settings(self, prefs=None, dictionaries_changed=False): # {{{ prefs = prefs or tprefs self.setAcceptDrops(prefs.get("editor_accepts_drops", True)) self.setLineWrapMode(QPlainTextEdit.WidgetWidth if prefs["editor_line_wrap"] else QPlainTextEdit.NoWrap) theme = get_theme(prefs["editor_theme"]) self.apply_theme(theme) w = self.fontMetrics() self.space_width = w.width(" ") self.tw = ( self.smarts.override_tab_stop_width if self.smarts.override_tab_stop_width is not None else prefs["editor_tab_stop_width"] ) self.setTabStopWidth(self.tw * self.space_width) if dictionaries_changed: self.highlighter.rehighlight() def apply_theme(self, theme): self.theme = theme pal = self.palette() pal.setColor(pal.Base, theme_color(theme, "Normal", "bg")) pal.setColor(pal.AlternateBase, theme_color(theme, "CursorLine", "bg")) pal.setColor(pal.Text, theme_color(theme, "Normal", "fg")) pal.setColor(pal.Highlight, theme_color(theme, "Visual", "bg")) pal.setColor(pal.HighlightedText, theme_color(theme, "Visual", "fg")) self.setPalette(pal) self.tooltip_palette = pal = QPalette() pal.setColor(pal.ToolTipBase, theme_color(theme, "Tooltip", "bg")) pal.setColor(pal.ToolTipText, theme_color(theme, "Tooltip", "fg")) self.line_number_palette = pal = QPalette() pal.setColor(pal.Base, theme_color(theme, "LineNr", "bg")) pal.setColor(pal.Text, theme_color(theme, "LineNr", "fg")) pal.setColor(pal.BrightText, theme_color(theme, "LineNrC", "fg")) self.match_paren_format = theme_format(theme, "MatchParen") font = self.font() ff = tprefs["editor_font_family"] if ff is None: ff = default_font_family() font.setFamily(ff) font.setPointSize(tprefs["editor_font_size"]) self.tooltip_font = QFont(font) self.tooltip_font.setPointSize(font.pointSize() - 1) self.setFont(font) self.highlighter.apply_theme(theme) w = self.fontMetrics() self.number_width = max(map(lambda x: w.width(str(x)), xrange(10))) self.size_hint = QSize(self.expected_geometry[0] * w.averageCharWidth(), self.expected_geometry[1] * w.height()) self.highlight_color = theme_color(theme, "HighlightRegion", "bg") self.highlight_cursor_line() self.completion_popup.clear_caches(), self.completion_popup.update() # }}} def load_text(self, text, syntax="html", process_template=False, doc_name=None): self.syntax = syntax self.highlighter = get_highlighter(syntax)() self.highlighter.apply_theme(self.theme) self.highlighter.set_document(self.document(), doc_name=doc_name) sclass = get_smarts(syntax) if sclass is not None: self.smarts = sclass(self) if self.smarts.override_tab_stop_width is not None: self.tw = self.smarts.override_tab_stop_width self.setTabStopWidth(self.tw * self.space_width) self.setPlainText(unicodedata.normalize("NFC", unicode(text))) if process_template and QPlainTextEdit.find(self, "%CURSOR%"): c = self.textCursor() c.insertText("") def change_document_name(self, newname): self.highlighter.doc_name = newname self.highlighter.rehighlight() # Ensure links are checked w.r.t. the new name correctly def replace_text(self, text): c = self.textCursor() pos = c.position() c.beginEditBlock() c.clearSelection() c.select(c.Document) c.insertText(unicodedata.normalize("NFC", text)) c.endEditBlock() c.setPosition(min(pos, len(text))) self.setTextCursor(c) self.ensureCursorVisible() def simple_replace(self, text, cursor=None): c = cursor or self.textCursor() c.insertText(unicodedata.normalize("NFC", text)) self.setTextCursor(c) def go_to_line(self, lnum, col=None): lnum = max(1, min(self.blockCount(), lnum)) c = self.textCursor() c.clearSelection() c.movePosition(c.Start) c.movePosition(c.NextBlock, n=lnum - 1) c.movePosition(c.StartOfLine) c.movePosition(c.EndOfLine, c.KeepAnchor) text = unicode(c.selectedText()).rstrip("\0") if col is None: c.movePosition(c.StartOfLine) lt = text.lstrip() if text and lt and lt != text: c.movePosition(c.NextWord) else: c.setPosition(c.block().position() + col) if c.blockNumber() + 1 > lnum: # We have moved past the end of the line c.setPosition(c.block().position()) c.movePosition(c.EndOfBlock) self.setTextCursor(c) self.ensureCursorVisible() def update_extra_selections(self, instant=True): sel = [] if self.current_cursor_line is not None: sel.append(self.current_cursor_line) if self.current_search_mark is not None: sel.append(self.current_search_mark) if instant and not self.highlighter.has_requests and self.smarts is not None: sel.extend(self.smarts.get_extra_selections(self)) self.smart_highlighting_updated.emit() else: self.smarts_highlight_timer.start() self.setExtraSelections(sel) # Search and replace {{{ def mark_selected_text(self): sel = QTextEdit.ExtraSelection() sel.format.setBackground(self.highlight_color) sel.cursor = self.textCursor() if sel.cursor.hasSelection(): self.current_search_mark = sel c = self.textCursor() c.clearSelection() self.setTextCursor(c) else: self.current_search_mark = None self.update_extra_selections() def find_in_marked(self, pat, wrap=False, save_match=None): if self.current_search_mark is None: return False csm = self.current_search_mark.cursor reverse = pat.flags & regex.REVERSE c = self.textCursor() c.clearSelection() m_start = min(csm.position(), csm.anchor()) m_end = max(csm.position(), csm.anchor()) if c.position() < m_start: c.setPosition(m_start) if c.position() > m_end: c.setPosition(m_end) pos = m_start if reverse else m_end if wrap: pos = m_end if reverse else m_start c.setPosition(pos, c.KeepAnchor) raw = unicode(c.selectedText()).replace(PARAGRAPH_SEPARATOR, "\n").rstrip("\0") m = pat.search(raw) if m is None: return False start, end = m.span() if start == end: return False if wrap: if reverse: textpos = c.anchor() start, end = textpos + end, textpos + start else: start, end = m_start + start, m_start + end else: if reverse: start, end = m_start + end, m_start + start else: start, end = c.anchor() + start, c.anchor() + end c.clearSelection() c.setPosition(start) c.setPosition(end, c.KeepAnchor) self.setTextCursor(c) # Center search result on screen self.centerCursor() if save_match is not None: self.saved_matches[save_match] = (pat, m) return True def all_in_marked(self, pat, template=None): if self.current_search_mark is None: return 0 c = self.current_search_mark.cursor raw = unicode(c.selectedText()).replace(PARAGRAPH_SEPARATOR, "\n").rstrip("\0") if template is None: count = len(pat.findall(raw)) else: from calibre.gui2.tweak_book.function_replace import Function repl_is_func = isinstance(template, Function) if repl_is_func: template.init_env() raw, count = pat.subn(template, raw) if repl_is_func: from calibre.gui2.tweak_book.search import show_function_debug_output template.end() show_function_debug_output(template) if count > 0: start_pos = min(c.anchor(), c.position()) c.insertText(raw) end_pos = max(c.anchor(), c.position()) c.setPosition(start_pos), c.setPosition(end_pos, c.KeepAnchor) self.update_extra_selections() return count def smart_comment(self): from calibre.gui2.tweak_book.editor.comments import smart_comment smart_comment(self, self.syntax) def find(self, pat, wrap=False, marked=False, complete=False, save_match=None): if marked: return self.find_in_marked(pat, wrap=wrap, save_match=save_match) reverse = pat.flags & regex.REVERSE c = self.textCursor() c.clearSelection() if complete: # Search the entire text c.movePosition(c.End if reverse else c.Start) pos = c.Start if reverse else c.End if wrap and not complete: pos = c.End if reverse else c.Start c.movePosition(pos, c.KeepAnchor) raw = unicode(c.selectedText()).replace(PARAGRAPH_SEPARATOR, "\n").rstrip("\0") m = pat.search(raw) if m is None: return False start, end = m.span() if start == end: return False if wrap and not complete: if reverse: textpos = c.anchor() start, end = textpos + end, textpos + start else: if reverse: # Put the cursor at the start of the match start, end = end, start else: textpos = c.anchor() start, end = textpos + start, textpos + end c.clearSelection() c.setPosition(start) c.setPosition(end, c.KeepAnchor) self.setTextCursor(c) # Center search result on screen self.centerCursor() if save_match is not None: self.saved_matches[save_match] = (pat, m) return True def find_spell_word(self, original_words, lang, from_cursor=True, center_on_cursor=True): c = self.textCursor() c.setPosition(c.position()) if not from_cursor: c.movePosition(c.Start) c.movePosition(c.End, c.KeepAnchor) def find_first_word(haystack): match_pos, match_word = -1, None for w in original_words: idx = index_of(w, haystack, lang=lang) if idx > -1 and (match_pos == -1 or match_pos > idx): match_pos, match_word = idx, w return match_pos, match_word while True: text = unicode(c.selectedText()).rstrip("\0") idx, word = find_first_word(text) if idx == -1: return False c.setPosition(c.anchor() + idx) c.setPosition(c.position() + string_length(word), c.KeepAnchor) if self.smarts.verify_for_spellcheck(c, self.highlighter): self.setTextCursor(c) if center_on_cursor: self.centerCursor() return True c.setPosition(c.position()) c.movePosition(c.End, c.KeepAnchor) return False def find_next_spell_error(self, from_cursor=True): c = self.textCursor() if not from_cursor: c.movePosition(c.Start) block = c.block() while block.isValid(): for r in block.layout().additionalFormats(): if r.format.property(SPELL_PROPERTY): if not from_cursor or block.position() + r.start + r.length > c.position(): c.setPosition(block.position() + r.start) c.setPosition(c.position() + r.length, c.KeepAnchor) self.setTextCursor(c) return True block = block.next() return False def replace(self, pat, template, saved_match="gui"): c = self.textCursor() raw = unicode(c.selectedText()).replace(PARAGRAPH_SEPARATOR, "\n").rstrip("\0") m = pat.fullmatch(raw) if m is None: # This can happen if either the user changed the selected text or # the search expression uses lookahead/lookbehind operators. See if # the saved match matches the currently selected text and # use it, if so. if saved_match is not None and saved_match in self.saved_matches: saved_pat, saved = self.saved_matches.pop(saved_match) if saved_pat == pat and saved.group() == raw: m = saved if m is None: return False if callable(template): text = template(m) else: text = m.expand(template) c.insertText(text) return True def go_to_anchor(self, anchor): if anchor is TOP: c = self.textCursor() c.movePosition(c.Start) self.setTextCursor(c) return True base = r"""%%s\s*=\s*['"]{0,1}%s""" % regex.escape(anchor) raw = unicode(self.toPlainText()) m = regex.search(base % "id", raw) if m is None: m = regex.search(base % "name", raw) if m is not None: c = self.textCursor() c.setPosition(m.start()) self.setTextCursor(c) return True return False # }}} # Line numbers and cursor line {{{ def highlight_cursor_line(self): sel = QTextEdit.ExtraSelection() sel.format.setBackground(self.palette().alternateBase()) sel.format.setProperty(QTextFormat.FullWidthSelection, True) sel.cursor = self.textCursor() sel.cursor.clearSelection() self.current_cursor_line = sel self.update_extra_selections(instant=False) # Update the cursor line's line number in the line number area try: self.line_number_area.update( 0, self.last_current_lnum[0], self.line_number_area.width(), self.last_current_lnum[1] ) except AttributeError: pass block = self.textCursor().block() top = int(self.blockBoundingGeometry(block).translated(self.contentOffset()).top()) height = int(self.blockBoundingRect(block).height()) self.line_number_area.update(0, top, self.line_number_area.width(), height) def update_line_number_area_width(self, block_count=0): self.gutter_width = self.line_number_area_width() self.setViewportMargins(self.gutter_width, 0, 0, 0) def line_number_area_width(self): digits = 1 limit = max(1, self.blockCount()) while limit >= 10: limit /= 10 digits += 1 return 8 + self.number_width * digits def update_line_number_area(self, rect, dy): if dy: self.line_number_area.scroll(0, dy) else: self.line_number_area.update(0, rect.y(), self.line_number_area.width(), rect.height()) if rect.contains(self.viewport().rect()): self.update_line_number_area_width() def resizeEvent(self, ev): QPlainTextEdit.resizeEvent(self, ev) cr = self.contentsRect() self.line_number_area.setGeometry(QRect(cr.left(), cr.top(), self.line_number_area_width(), cr.height())) def paint_line_numbers(self, ev): painter = QPainter(self.line_number_area) painter.fillRect(ev.rect(), self.line_number_palette.color(QPalette.Base)) block = self.firstVisibleBlock() num = block.blockNumber() top = int(self.blockBoundingGeometry(block).translated(self.contentOffset()).top()) bottom = top + int(self.blockBoundingRect(block).height()) current = self.textCursor().block().blockNumber() painter.setPen(self.line_number_palette.color(QPalette.Text)) while block.isValid() and top <= ev.rect().bottom(): if block.isVisible() and bottom >= ev.rect().top(): if current == num: painter.save() painter.setPen(self.line_number_palette.color(QPalette.BrightText)) f = QFont(self.font()) f.setBold(True) painter.setFont(f) self.last_current_lnum = (top, bottom - top) painter.drawText( 0, top, self.line_number_area.width() - 5, self.fontMetrics().height(), Qt.AlignRight, str(num + 1) ) if current == num: painter.restore() block = block.next() top = bottom bottom = top + int(self.blockBoundingRect(block).height()) num += 1 # }}} def event(self, ev): if ev.type() == ev.ToolTip: self.show_tooltip(ev) return True if ev.type() == ev.ShortcutOverride: # Let the global cut/copy/paste/undo/redo shortcuts work, this avoids the nbsp # problem as well, since they use the overridden copy() method # instead of the one from Qt, and allows proper customization # of the shortcuts if ev in (QKeySequence.Copy, QKeySequence.Cut, QKeySequence.Paste, QKeySequence.Undo, QKeySequence.Redo): ev.ignore() return True # This is used to convert typed hex codes into unicode # characters if ev.key() == Qt.Key_X and ev.modifiers() == Qt.AltModifier: ev.accept() return True return QPlainTextEdit.event(self, ev) def text_for_range(self, block, r): c = self.textCursor() c.setPosition(block.position() + r.start) c.setPosition(c.position() + r.length, c.KeepAnchor) return unicode(c.selectedText()) def spellcheck_locale_for_cursor(self, c): with store_locale: formats = self.highlighter.parse_single_block(c.block())[0] pos = c.positionInBlock() for r in formats: if r.start <= pos <= r.start + r.length and r.format.property(SPELL_PROPERTY): return r.format.property(SPELL_LOCALE_PROPERTY) def recheck_word(self, word, locale): c = self.textCursor() c.movePosition(c.Start) block = c.block() while block.isValid(): for r in block.layout().additionalFormats(): if r.format.property(SPELL_PROPERTY) and self.text_for_range(block, r) == word: self.highlighter.reformat_block(block) break block = block.next() # Tooltips {{{ def syntax_range_for_cursor(self, cursor): if cursor.isNull(): return pos = cursor.positionInBlock() for r in cursor.block().layout().additionalFormats(): if r.start <= pos <= r.start + r.length and r.format.property(SYNTAX_PROPERTY): return r def syntax_format_for_cursor(self, cursor): return getattr(self.syntax_range_for_cursor(cursor), "format", None) def show_tooltip(self, ev): c = self.cursorForPosition(ev.pos()) fmt = self.syntax_format_for_cursor(c) if fmt is not None: tt = unicode(fmt.toolTip()) if tt: QToolTip.setFont(self.tooltip_font) QToolTip.setPalette(self.tooltip_palette) QToolTip.showText(ev.globalPos(), textwrap.fill(tt)) return QToolTip.hideText() ev.ignore() # }}} def link_for_position(self, pos): c = self.cursorForPosition(pos) r = self.syntax_range_for_cursor(c) if r is not None and r.format.property(LINK_PROPERTY): return self.text_for_range(c.block(), r) def mousePressEvent(self, ev): if self.completion_popup.isVisible() and not self.completion_popup.rect().contains(ev.pos()): # For some reason using eventFilter for this does not work, so we # implement it here self.completion_popup.abort() if ev.modifiers() & Qt.CTRL: url = self.link_for_position(ev.pos()) if url is not None: ev.accept() self.link_clicked.emit(url) return return PlainTextEdit.mousePressEvent(self, ev) def get_range_inside_tag(self): c = self.textCursor() left = min(c.anchor(), c.position()) right = max(c.anchor(), c.position()) # For speed we use QPlainTextEdit's toPlainText as we dont care about # spaces in this context raw = unicode(QPlainTextEdit.toPlainText(self)) # Make sure the left edge is not within a <> gtpos = raw.find(">", left) ltpos = raw.find("<", left) if gtpos < ltpos: left = gtpos + 1 if gtpos > -1 else left right = max(left, right) if right != left: gtpos = raw.find(">", right) ltpos = raw.find("<", right) if ltpos > gtpos: ltpos = raw.rfind("<", left, right + 1) right = max(ltpos, left) return left, right def format_text(self, formatting): if self.syntax != "html": return if formatting.startswith("justify_"): return self.smarts.set_text_alignment(self, formatting.partition("_")[-1]) color = "currentColor" if formatting in {"color", "background-color"}: color = QColorDialog.getColor( QColor(Qt.black if formatting == "color" else Qt.white), self, _("Choose color"), QColorDialog.ShowAlphaChannel, ) if not color.isValid(): return r, g, b, a = color.getRgb() if a == 255: color = "rgb(%d, %d, %d)" % (r, g, b) else: color = "rgba(%d, %d, %d, %.2g)" % (r, g, b, a / 255) prefix, suffix = { "bold": ("<b>", "</b>"), "italic": ("<i>", "</i>"), "underline": ("<u>", "</u>"), "strikethrough": ("<strike>", "</strike>"), "superscript": ("<sup>", "</sup>"), "subscript": ("<sub>", "</sub>"), "color": ('<span style="color: %s">' % color, "</span>"), "background-color": ('<span style="background-color: %s">' % color, "</span>"), }[formatting] left, right = self.get_range_inside_tag() c = self.textCursor() c.setPosition(left) c.setPosition(right, c.KeepAnchor) prev_text = unicode(c.selectedText()).rstrip("\0") c.insertText(prefix + prev_text + suffix) if prev_text: right = c.position() c.setPosition(left) c.setPosition(right, c.KeepAnchor) else: c.setPosition(c.position() - len(suffix)) self.setTextCursor(c) def insert_image(self, href, fullpage=False, preserve_aspect_ratio=False): c = self.textCursor() template, alt = "url(%s)", "" left = min(c.position(), c.anchor) if self.syntax == "html": left, right = self.get_range_inside_tag() c.setPosition(left) c.setPosition(right, c.KeepAnchor) href = prepare_string_for_xml(href, True) if fullpage: template = """\ <div style="page-break-before:always; page-break-after:always; page-break-inside:avoid">\ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" \ version="1.1" width="100%%" height="100%%" viewBox="0 0 1200 1600" preserveAspectRatio="{}">\ <image width="1200" height="1600" xlink:href="%s"/>\ </svg></div>""".format( "xMidYMid meet" if preserve_aspect_ratio else "none" ) else: alt = _("Image") template = '<img alt="{0}" src="%s" />'.format(alt) text = template % href c.insertText(text) if self.syntax == "html" and not fullpage: c.setPosition(left + 10) c.setPosition(c.position() + len(alt), c.KeepAnchor) else: c.setPosition(left) c.setPosition(left + len(text), c.KeepAnchor) self.setTextCursor(c) def insert_hyperlink(self, target, text): if hasattr(self.smarts, "insert_hyperlink"): self.smarts.insert_hyperlink(self, target, text) def insert_tag(self, tag): if hasattr(self.smarts, "insert_tag"): self.smarts.insert_tag(self, tag) def keyPressEvent(self, ev): if ev.key() == Qt.Key_X and ev.modifiers() == Qt.AltModifier: if self.replace_possible_unicode_sequence(): ev.accept() return if ev.key() == Qt.Key_Insert: self.setOverwriteMode(self.overwriteMode() ^ True) ev.accept() return if self.snippet_manager.handle_key_press(ev): self.completion_popup.hide() return if self.smarts.handle_key_press(ev, self): self.handle_keypress_completion(ev) return QPlainTextEdit.keyPressEvent(self, ev) self.handle_keypress_completion(ev) def handle_keypress_completion(self, ev): if self.request_completion is None: return code = ev.key() if code in ( 0, Qt.Key_unknown, Qt.Key_Shift, Qt.Key_Control, Qt.Key_Alt, Qt.Key_Meta, Qt.Key_AltGr, Qt.Key_CapsLock, Qt.Key_NumLock, Qt.Key_ScrollLock, Qt.Key_Up, Qt.Key_Down, ): # We ignore up/down arrow so as to not break scrolling through the # text with the arrow keys return result = self.smarts.get_completion_data(self, ev) if result is None: self.last_completion_request += 1 else: self.last_completion_request = self.request_completion(*result) self.completion_popup.mark_completion(self, None if result is None else result[-1]) def handle_completion_result(self, result): if result.request_id[0] >= self.last_completion_request: self.completion_popup.handle_result(result) def clear_completion_cache(self): if self.request_completion is not None and self.completion_doc_name: self.request_completion(None, "file:" + self.completion_doc_name) def replace_possible_unicode_sequence(self): c = self.textCursor() has_selection = c.hasSelection() if has_selection: text = unicode(c.selectedText()).rstrip("\0") else: c.setPosition(c.position() - min(c.positionInBlock(), 6), c.KeepAnchor) text = unicode(c.selectedText()).rstrip("\0") m = re.search(r"[a-fA-F0-9]{2,6}$", text) if m is None: return False text = m.group() try: num = int(text, 16) except ValueError: return False if num > 0x10FFFF or num < 1: return False end_pos = max(c.anchor(), c.position()) c.setPosition(end_pos - len(text)), c.setPosition(end_pos, c.KeepAnchor) c.insertText(safe_chr(num)) return True def select_all(self): c = self.textCursor() c.clearSelection() c.setPosition(0) c.movePosition(c.End, c.KeepAnchor) self.setTextCursor(c) def rename_block_tag(self, new_name): if hasattr(self.smarts, "rename_block_tag"): self.smarts.rename_block_tag(self, new_name) def current_tag(self, for_position_sync=True): return self.smarts.cursor_position_with_sourceline(self.textCursor(), for_position_sync=for_position_sync) def goto_sourceline(self, sourceline, tags, attribute=None): return self.smarts.goto_sourceline(self, sourceline, tags, attribute=attribute) def get_tag_contents(self): c = self.smarts.get_inner_HTML(self) if c is not None: return self.selected_text_from_cursor(c) def goto_css_rule(self, rule_address, sourceline_address=None): from calibre.gui2.tweak_book.editor.smarts.css import find_rule block = None if self.syntax == "css": raw = unicode(self.toPlainText()) line, col = find_rule(raw, rule_address) if line is not None: block = self.document().findBlockByNumber(line - 1) elif sourceline_address is not None: sourceline, tags = sourceline_address if self.goto_sourceline(sourceline, tags): c = self.textCursor() c.setPosition(c.position() + 1) self.setTextCursor(c) raw = self.get_tag_contents() line, col = find_rule(raw, rule_address) if line is not None: block = self.document().findBlockByNumber(c.blockNumber() + line - 1) if block is not None and block.isValid(): c = self.textCursor() c.setPosition(block.position() + col) self.setTextCursor(c) def change_case(self, action, cursor=None): cursor = cursor or self.textCursor() text = self.selected_text_from_cursor(cursor) text = {"lower": lower, "upper": upper, "capitalize": capitalize, "title": titlecase, "swap": swapcase}[action]( text ) cursor.insertText(text) self.setTextCursor(cursor)
class TextEdit(PlainTextEdit): link_clicked = pyqtSignal(object) smart_highlighting_updated = pyqtSignal() def __init__(self, parent=None, expected_geometry=(100, 50)): PlainTextEdit.__init__(self, parent) self.snippet_manager = SnippetManager(self) self.completion_popup = CompletionPopup(self) self.request_completion = self.completion_doc_name = None self.clear_completion_cache_timer = t = QTimer(self) t.setInterval(5000), t.timeout.connect( self.clear_completion_cache), t.setSingleShot(True) self.textChanged.connect(t.start) self.last_completion_request = -1 self.gutter_width = 0 self.tw = 2 self.expected_geometry = expected_geometry self.saved_matches = {} self.syntax = None self.smarts = NullSmarts(self) self.current_cursor_line = None self.current_search_mark = None self.smarts_highlight_timer = t = QTimer() t.setInterval(750), t.setSingleShot(True), t.timeout.connect( self.update_extra_selections) self.highlighter = SyntaxHighlighter() self.line_number_area = LineNumbers(self) self.apply_settings() self.setMouseTracking(True) self.cursorPositionChanged.connect(self.highlight_cursor_line) self.blockCountChanged[int].connect(self.update_line_number_area_width) self.updateRequest.connect(self.update_line_number_area) def get_droppable_files(self, md): def is_mt_ok(mt): return self.syntax == 'html' and (mt in OEB_DOCS or mt in OEB_STYLES or mt.startswith('image/')) if md.hasFormat(CONTAINER_DND_MIMETYPE): for line in bytes(md.data(CONTAINER_DND_MIMETYPE)).decode( 'utf-8').splitlines(): mt = current_container().mime_map.get( line, 'application/octet-stream') if is_mt_ok(mt): yield line, mt, True return for qurl in md.urls(): if qurl.isLocalFile() and os.access(qurl.toLocalFile(), os.R_OK): path = qurl.toLocalFile() mt = guess_type(path) if is_mt_ok(mt): yield path, mt, False def canInsertFromMimeData(self, md): if md.hasText() or (md.hasHtml() and self.syntax == 'html') or md.hasImage(): return True elif tuple(self.get_droppable_files(md)): return True return False def insertFromMimeData(self, md): files = tuple(self.get_droppable_files(md)) base = self.highlighter.doc_name or None def get_name(name): return get_recommended_folders(current_container(), (name, ))[name] + '/' + name def get_href(name): return current_container().name_to_href(name, base) def insert_text(text): c = self.textCursor() c.insertText(text) self.setTextCursor(c) def add_file(name, data, mt=None): from calibre.gui2.tweak_book.boss import get_boss name = current_container().add_file(name, data, media_type=mt, modify_name_if_needed=True) get_boss().refresh_file_list() return name if files: for path, mt, is_name in files: if is_name: name = path else: name = get_name(os.path.basename(path)) with lopen(path, 'rb') as f: name = add_file(name, f.read(), mt) href = get_href(name) if mt.startswith('image/'): self.insert_image(href) elif mt in OEB_STYLES: insert_text( '<link href="{}" rel="stylesheet" type="text/css"/>'. format(href)) elif mt in OEB_DOCS: self.insert_hyperlink(href, name) return if md.hasImage(): img = md.imageData() if img.isValid(): data = image_to_data(img, fmt='PNG') name = add_file(get_name('dropped_image.png', data)) self.insert_image(get_href(name)) return if md.hasHtml(): insert_text(md.html()) return if md.hasText(): return insert_text(md.text()) @dynamic_property def is_modified(self): ''' True if the document has been modified since it was loaded or since the last time is_modified was set to False. ''' def fget(self): return self.document().isModified() def fset(self, val): self.document().setModified(bool(val)) return property(fget=fget, fset=fset) def sizeHint(self): return self.size_hint def apply_settings(self, prefs=None, dictionaries_changed=False): # {{{ prefs = prefs or tprefs self.setAcceptDrops(prefs.get('editor_accepts_drops', True)) self.setLineWrapMode( QPlainTextEdit.WidgetWidth if prefs['editor_line_wrap'] else QPlainTextEdit.NoWrap) theme = get_theme(prefs['editor_theme']) self.apply_theme(theme) w = self.fontMetrics() self.space_width = w.width(' ') self.tw = self.smarts.override_tab_stop_width if self.smarts.override_tab_stop_width is not None else prefs[ 'editor_tab_stop_width'] self.setTabStopWidth(self.tw * self.space_width) if dictionaries_changed: self.highlighter.rehighlight() def apply_theme(self, theme): self.theme = theme pal = self.palette() pal.setColor(pal.Base, theme_color(theme, 'Normal', 'bg')) pal.setColor(pal.AlternateBase, theme_color(theme, 'CursorLine', 'bg')) pal.setColor(pal.Text, theme_color(theme, 'Normal', 'fg')) pal.setColor(pal.Highlight, theme_color(theme, 'Visual', 'bg')) pal.setColor(pal.HighlightedText, theme_color(theme, 'Visual', 'fg')) self.setPalette(pal) self.tooltip_palette = pal = QPalette() pal.setColor(pal.ToolTipBase, theme_color(theme, 'Tooltip', 'bg')) pal.setColor(pal.ToolTipText, theme_color(theme, 'Tooltip', 'fg')) self.line_number_palette = pal = QPalette() pal.setColor(pal.Base, theme_color(theme, 'LineNr', 'bg')) pal.setColor(pal.Text, theme_color(theme, 'LineNr', 'fg')) pal.setColor(pal.BrightText, theme_color(theme, 'LineNrC', 'fg')) self.match_paren_format = theme_format(theme, 'MatchParen') font = self.font() ff = tprefs['editor_font_family'] if ff is None: ff = default_font_family() font.setFamily(ff) font.setPointSize(tprefs['editor_font_size']) self.tooltip_font = QFont(font) self.tooltip_font.setPointSize(font.pointSize() - 1) self.setFont(font) self.highlighter.apply_theme(theme) w = self.fontMetrics() self.number_width = max(map(lambda x: w.width(str(x)), xrange(10))) self.size_hint = QSize( self.expected_geometry[0] * w.averageCharWidth(), self.expected_geometry[1] * w.height()) self.highlight_color = theme_color(theme, 'HighlightRegion', 'bg') self.highlight_cursor_line() self.completion_popup.clear_caches(), self.completion_popup.update() # }}} def load_text(self, text, syntax='html', process_template=False, doc_name=None): self.syntax = syntax self.highlighter = get_highlighter(syntax)() self.highlighter.apply_theme(self.theme) self.highlighter.set_document(self.document(), doc_name=doc_name) sclass = get_smarts(syntax) if sclass is not None: self.smarts = sclass(self) if self.smarts.override_tab_stop_width is not None: self.tw = self.smarts.override_tab_stop_width self.setTabStopWidth(self.tw * self.space_width) self.setPlainText(unicodedata.normalize('NFC', unicode(text))) if process_template and QPlainTextEdit.find(self, '%CURSOR%'): c = self.textCursor() c.insertText('') def change_document_name(self, newname): self.highlighter.doc_name = newname self.highlighter.rehighlight( ) # Ensure links are checked w.r.t. the new name correctly def replace_text(self, text): c = self.textCursor() pos = c.position() c.beginEditBlock() c.clearSelection() c.select(c.Document) c.insertText(unicodedata.normalize('NFC', text)) c.endEditBlock() c.setPosition(min(pos, len(text))) self.setTextCursor(c) self.ensureCursorVisible() def simple_replace(self, text, cursor=None): c = cursor or self.textCursor() c.insertText(unicodedata.normalize('NFC', text)) self.setTextCursor(c) def go_to_line(self, lnum, col=None): lnum = max(1, min(self.blockCount(), lnum)) c = self.textCursor() c.clearSelection() c.movePosition(c.Start) c.movePosition(c.NextBlock, n=lnum - 1) c.movePosition(c.StartOfLine) c.movePosition(c.EndOfLine, c.KeepAnchor) text = unicode(c.selectedText()).rstrip('\0') if col is None: c.movePosition(c.StartOfLine) lt = text.lstrip() if text and lt and lt != text: c.movePosition(c.NextWord) else: c.setPosition(c.block().position() + col) if c.blockNumber() + 1 > lnum: # We have moved past the end of the line c.setPosition(c.block().position()) c.movePosition(c.EndOfBlock) self.setTextCursor(c) self.ensureCursorVisible() def update_extra_selections(self, instant=True): sel = [] if self.current_cursor_line is not None: sel.append(self.current_cursor_line) if self.current_search_mark is not None: sel.append(self.current_search_mark) if instant and not self.highlighter.has_requests and self.smarts is not None: sel.extend(self.smarts.get_extra_selections(self)) self.smart_highlighting_updated.emit() else: self.smarts_highlight_timer.start() self.setExtraSelections(sel) # Search and replace {{{ def mark_selected_text(self): sel = QTextEdit.ExtraSelection() sel.format.setBackground(self.highlight_color) sel.cursor = self.textCursor() if sel.cursor.hasSelection(): self.current_search_mark = sel c = self.textCursor() c.clearSelection() self.setTextCursor(c) else: self.current_search_mark = None self.update_extra_selections() def find_in_marked(self, pat, wrap=False, save_match=None): if self.current_search_mark is None: return False csm = self.current_search_mark.cursor reverse = pat.flags & regex.REVERSE c = self.textCursor() c.clearSelection() m_start = min(csm.position(), csm.anchor()) m_end = max(csm.position(), csm.anchor()) if c.position() < m_start: c.setPosition(m_start) if c.position() > m_end: c.setPosition(m_end) pos = m_start if reverse else m_end if wrap: pos = m_end if reverse else m_start c.setPosition(pos, c.KeepAnchor) raw = unicode(c.selectedText()).replace(PARAGRAPH_SEPARATOR, '\n').rstrip('\0') m = pat.search(raw) if m is None: return False start, end = m.span() if start == end: return False if wrap: if reverse: textpos = c.anchor() start, end = textpos + end, textpos + start else: start, end = m_start + start, m_start + end else: if reverse: start, end = m_start + end, m_start + start else: start, end = c.anchor() + start, c.anchor() + end c.clearSelection() c.setPosition(start) c.setPosition(end, c.KeepAnchor) self.setTextCursor(c) # Center search result on screen self.centerCursor() if save_match is not None: self.saved_matches[save_match] = (pat, m) return True def all_in_marked(self, pat, template=None): if self.current_search_mark is None: return 0 c = self.current_search_mark.cursor raw = unicode(c.selectedText()).replace(PARAGRAPH_SEPARATOR, '\n').rstrip('\0') if template is None: count = len(pat.findall(raw)) else: from calibre.gui2.tweak_book.function_replace import Function repl_is_func = isinstance(template, Function) if repl_is_func: template.init_env() raw, count = pat.subn(template, raw) if repl_is_func: from calibre.gui2.tweak_book.search import show_function_debug_output if getattr(template.func, 'append_final_output_to_marked', False): retval = template.end() if retval: raw += unicode(retval) else: template.end() show_function_debug_output(template) if count > 0: start_pos = min(c.anchor(), c.position()) c.insertText(raw) end_pos = max(c.anchor(), c.position()) c.setPosition(start_pos), c.setPosition(end_pos, c.KeepAnchor) self.update_extra_selections() return count def smart_comment(self): from calibre.gui2.tweak_book.editor.comments import smart_comment smart_comment(self, self.syntax) def sort_css(self): from calibre.gui2.dialogs.confirm_delete import confirm if confirm(_( 'Sorting CSS rules can in rare cases change the effective styles applied to the book.' ' Are you sure you want to proceed?'), 'edit-book-confirm-sort-css', parent=self, config_set=tprefs): c = self.textCursor() c.beginEditBlock() c.movePosition(c.Start), c.movePosition(c.End, c.KeepAnchor) text = unicode(c.selectedText()).replace(PARAGRAPH_SEPARATOR, '\n').rstrip('\0') from calibre.ebooks.oeb.polish.css import sort_sheet text = sort_sheet(current_container(), text).cssText c.insertText(text) c.movePosition(c.Start) c.endEditBlock() self.setTextCursor(c) def find(self, pat, wrap=False, marked=False, complete=False, save_match=None): if marked: return self.find_in_marked(pat, wrap=wrap, save_match=save_match) reverse = pat.flags & regex.REVERSE c = self.textCursor() c.clearSelection() if complete: # Search the entire text c.movePosition(c.End if reverse else c.Start) pos = c.Start if reverse else c.End if wrap and not complete: pos = c.End if reverse else c.Start c.movePosition(pos, c.KeepAnchor) raw = unicode(c.selectedText()).replace(PARAGRAPH_SEPARATOR, '\n').rstrip('\0') m = pat.search(raw) if m is None: return False start, end = m.span() if start == end: return False if wrap and not complete: if reverse: textpos = c.anchor() start, end = textpos + end, textpos + start else: if reverse: # Put the cursor at the start of the match start, end = end, start else: textpos = c.anchor() start, end = textpos + start, textpos + end c.clearSelection() c.setPosition(start) c.setPosition(end, c.KeepAnchor) self.setTextCursor(c) # Center search result on screen self.centerCursor() if save_match is not None: self.saved_matches[save_match] = (pat, m) return True def find_text(self, pat, wrap=False, complete=False): reverse = pat.flags & regex.REVERSE c = self.textCursor() c.clearSelection() if complete: # Search the entire text c.movePosition(c.End if reverse else c.Start) pos = c.Start if reverse else c.End if wrap and not complete: pos = c.End if reverse else c.Start c.movePosition(pos, c.KeepAnchor) if hasattr(self.smarts, 'find_text'): self.highlighter.join() found, start, end = self.smarts.find_text(pat, c) if not found: return False else: raw = unicode(c.selectedText()).replace(PARAGRAPH_SEPARATOR, '\n').rstrip('\0') m = pat.search(raw) if m is None: return False start, end = m.span() if start == end: return False if reverse: start, end = end, start c.clearSelection() c.setPosition(start) c.setPosition(end, c.KeepAnchor) self.setTextCursor(c) # Center search result on screen self.centerCursor() return True def find_spell_word(self, original_words, lang, from_cursor=True, center_on_cursor=True): c = self.textCursor() c.setPosition(c.position()) if not from_cursor: c.movePosition(c.Start) c.movePosition(c.End, c.KeepAnchor) def find_first_word(haystack): match_pos, match_word = -1, None for w in original_words: idx = index_of(w, haystack, lang=lang) if idx > -1 and (match_pos == -1 or match_pos > idx): match_pos, match_word = idx, w return match_pos, match_word while True: text = unicode(c.selectedText()).rstrip('\0') idx, word = find_first_word(text) if idx == -1: return False c.setPosition(c.anchor() + idx) c.setPosition(c.position() + string_length(word), c.KeepAnchor) if self.smarts.verify_for_spellcheck(c, self.highlighter): self.highlighter.join() # Ensure highlighting is finished locale = self.spellcheck_locale_for_cursor(c) if not lang or not locale or (locale and lang == locale.langcode): self.setTextCursor(c) if center_on_cursor: self.centerCursor() return True c.setPosition(c.position()) c.movePosition(c.End, c.KeepAnchor) return False def find_next_spell_error(self, from_cursor=True): c = self.textCursor() if not from_cursor: c.movePosition(c.Start) block = c.block() while block.isValid(): for r in block.layout().additionalFormats(): if r.format.property(SPELL_PROPERTY): if not from_cursor or block.position( ) + r.start + r.length > c.position(): c.setPosition(block.position() + r.start) c.setPosition(c.position() + r.length, c.KeepAnchor) self.setTextCursor(c) return True block = block.next() return False def replace(self, pat, template, saved_match='gui'): c = self.textCursor() raw = unicode(c.selectedText()).replace(PARAGRAPH_SEPARATOR, '\n').rstrip('\0') m = pat.fullmatch(raw) if m is None: # This can happen if either the user changed the selected text or # the search expression uses lookahead/lookbehind operators. See if # the saved match matches the currently selected text and # use it, if so. if saved_match is not None and saved_match in self.saved_matches: saved_pat, saved = self.saved_matches.pop(saved_match) if saved_pat == pat and saved.group() == raw: m = saved if m is None: return False if callable(template): text = template(m) else: text = m.expand(template) c.insertText(text) return True def go_to_anchor(self, anchor): if anchor is TOP: c = self.textCursor() c.movePosition(c.Start) self.setTextCursor(c) return True base = r'''%%s\s*=\s*['"]{0,1}%s''' % regex.escape(anchor) raw = unicode(self.toPlainText()) m = regex.search(base % 'id', raw) if m is None: m = regex.search(base % 'name', raw) if m is not None: c = self.textCursor() c.setPosition(m.start()) self.setTextCursor(c) return True return False # }}} # Line numbers and cursor line {{{ def highlight_cursor_line(self): sel = QTextEdit.ExtraSelection() sel.format.setBackground(self.palette().alternateBase()) sel.format.setProperty(QTextFormat.FullWidthSelection, True) sel.cursor = self.textCursor() sel.cursor.clearSelection() self.current_cursor_line = sel self.update_extra_selections(instant=False) # Update the cursor line's line number in the line number area try: self.line_number_area.update(0, self.last_current_lnum[0], self.line_number_area.width(), self.last_current_lnum[1]) except AttributeError: pass block = self.textCursor().block() top = int( self.blockBoundingGeometry(block).translated( self.contentOffset()).top()) height = int(self.blockBoundingRect(block).height()) self.line_number_area.update(0, top, self.line_number_area.width(), height) def update_line_number_area_width(self, block_count=0): self.gutter_width = self.line_number_area_width() self.setViewportMargins(self.gutter_width, 0, 0, 0) def line_number_area_width(self): digits = 1 limit = max(1, self.blockCount()) while limit >= 10: limit /= 10 digits += 1 return 8 + self.number_width * digits def update_line_number_area(self, rect, dy): if dy: self.line_number_area.scroll(0, dy) else: self.line_number_area.update(0, rect.y(), self.line_number_area.width(), rect.height()) if rect.contains(self.viewport().rect()): self.update_line_number_area_width() def resizeEvent(self, ev): QPlainTextEdit.resizeEvent(self, ev) cr = self.contentsRect() self.line_number_area.setGeometry( QRect(cr.left(), cr.top(), self.line_number_area_width(), cr.height())) def paint_line_numbers(self, ev): painter = QPainter(self.line_number_area) painter.fillRect(ev.rect(), self.line_number_palette.color(QPalette.Base)) block = self.firstVisibleBlock() num = block.blockNumber() top = int( self.blockBoundingGeometry(block).translated( self.contentOffset()).top()) bottom = top + int(self.blockBoundingRect(block).height()) current = self.textCursor().block().blockNumber() painter.setPen(self.line_number_palette.color(QPalette.Text)) while block.isValid() and top <= ev.rect().bottom(): if block.isVisible() and bottom >= ev.rect().top(): if current == num: painter.save() painter.setPen( self.line_number_palette.color(QPalette.BrightText)) f = QFont(self.font()) f.setBold(True) painter.setFont(f) self.last_current_lnum = (top, bottom - top) painter.drawText(0, top, self.line_number_area.width() - 5, self.fontMetrics().height(), Qt.AlignRight, str(num + 1)) if current == num: painter.restore() block = block.next() top = bottom bottom = top + int(self.blockBoundingRect(block).height()) num += 1 # }}} def override_shortcut(self, ev): # Let the global cut/copy/paste/undo/redo shortcuts work, this avoids the nbsp # problem as well, since they use the overridden copy() method # instead of the one from Qt, and allows proper customization # of the shortcuts if ev in (QKeySequence.Copy, QKeySequence.Cut, QKeySequence.Paste, QKeySequence.Undo, QKeySequence.Redo): ev.ignore() return True # This is used to convert typed hex codes into unicode # characters if ev.key() == Qt.Key_X and ev.modifiers() == Qt.AltModifier: ev.accept() return True return PlainTextEdit.override_shortcut(self, ev) def text_for_range(self, block, r): c = self.textCursor() c.setPosition(block.position() + r.start) c.setPosition(c.position() + r.length, c.KeepAnchor) return unicode(c.selectedText()) def spellcheck_locale_for_cursor(self, c): with store_locale: formats = self.highlighter.parse_single_block(c.block())[0] pos = c.positionInBlock() for r in formats: if r.start <= pos <= r.start + r.length and r.format.property( SPELL_PROPERTY): return r.format.property(SPELL_LOCALE_PROPERTY) def recheck_word(self, word, locale): c = self.textCursor() c.movePosition(c.Start) block = c.block() while block.isValid(): for r in block.layout().additionalFormats(): if r.format.property(SPELL_PROPERTY) and self.text_for_range( block, r) == word: self.highlighter.reformat_block(block) break block = block.next() # Tooltips {{{ def syntax_range_for_cursor(self, cursor): if cursor.isNull(): return pos = cursor.positionInBlock() for r in cursor.block().layout().additionalFormats(): if r.start <= pos <= r.start + r.length and r.format.property( SYNTAX_PROPERTY): return r def syntax_format_for_cursor(self, cursor): return getattr(self.syntax_range_for_cursor(cursor), 'format', None) def show_tooltip(self, ev): c = self.cursorForPosition(ev.pos()) fmt = self.syntax_format_for_cursor(c) if fmt is not None: tt = unicode(fmt.toolTip()) if tt: QToolTip.setFont(self.tooltip_font) QToolTip.setPalette(self.tooltip_palette) QToolTip.showText(ev.globalPos(), textwrap.fill(tt)) return QToolTip.hideText() ev.ignore() # }}} def link_for_position(self, pos): c = self.cursorForPosition(pos) r = self.syntax_range_for_cursor(c) if r is not None and r.format.property(LINK_PROPERTY): return self.text_for_range(c.block(), r) def mousePressEvent(self, ev): if self.completion_popup.isVisible( ) and not self.completion_popup.rect().contains(ev.pos()): # For some reason using eventFilter for this does not work, so we # implement it here self.completion_popup.abort() if ev.modifiers() & Qt.CTRL: url = self.link_for_position(ev.pos()) if url is not None: ev.accept() self.link_clicked.emit(url) return return PlainTextEdit.mousePressEvent(self, ev) def get_range_inside_tag(self): c = self.textCursor() left = min(c.anchor(), c.position()) right = max(c.anchor(), c.position()) # For speed we use QPlainTextEdit's toPlainText as we dont care about # spaces in this context raw = unicode(QPlainTextEdit.toPlainText(self)) # Make sure the left edge is not within a <> gtpos = raw.find('>', left) ltpos = raw.find('<', left) if gtpos < ltpos: left = gtpos + 1 if gtpos > -1 else left right = max(left, right) if right != left: gtpos = raw.find('>', right) ltpos = raw.find('<', right) if ltpos > gtpos: ltpos = raw.rfind('<', left, right + 1) right = max(ltpos, left) return left, right def format_text(self, formatting): if self.syntax != 'html': return if formatting.startswith('justify_'): return self.smarts.set_text_alignment( self, formatting.partition('_')[-1]) color = 'currentColor' if formatting in {'color', 'background-color'}: color = QColorDialog.getColor( QColor(Qt.black if formatting == 'color' else Qt.white), self, _('Choose color'), QColorDialog.ShowAlphaChannel) if not color.isValid(): return r, g, b, a = color.getRgb() if a == 255: color = 'rgb(%d, %d, %d)' % (r, g, b) else: color = 'rgba(%d, %d, %d, %.2g)' % (r, g, b, a / 255) prefix, suffix = { 'bold': ('<b>', '</b>'), 'italic': ('<i>', '</i>'), 'underline': ('<u>', '</u>'), 'strikethrough': ('<strike>', '</strike>'), 'superscript': ('<sup>', '</sup>'), 'subscript': ('<sub>', '</sub>'), 'color': ('<span style="color: %s">' % color, '</span>'), 'background-color': ('<span style="background-color: %s">' % color, '</span>'), }[formatting] left, right = self.get_range_inside_tag() c = self.textCursor() c.setPosition(left) c.setPosition(right, c.KeepAnchor) prev_text = unicode(c.selectedText()).rstrip('\0') c.insertText(prefix + prev_text + suffix) if prev_text: right = c.position() c.setPosition(left) c.setPosition(right, c.KeepAnchor) else: c.setPosition(c.position() - len(suffix)) self.setTextCursor(c) def insert_image(self, href, fullpage=False, preserve_aspect_ratio=False): c = self.textCursor() template, alt = 'url(%s)', '' left = min(c.position(), c.anchor) if self.syntax == 'html': left, right = self.get_range_inside_tag() c.setPosition(left) c.setPosition(right, c.KeepAnchor) href = prepare_string_for_xml(href, True) if fullpage: template = '''\ <div style="page-break-before:always; page-break-after:always; page-break-inside:avoid">\ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" \ version="1.1" width="100%%" height="100%%" viewBox="0 0 1200 1600" preserveAspectRatio="{}">\ <image width="1200" height="1600" xlink:href="%s"/>\ </svg></div>'''.format('xMidYMid meet' if preserve_aspect_ratio else 'none') else: alt = _('Image') template = '<img alt="{0}" src="%s" />'.format(alt) text = template % href c.insertText(text) if self.syntax == 'html' and not fullpage: c.setPosition(left + 10) c.setPosition(c.position() + len(alt), c.KeepAnchor) else: c.setPosition(left) c.setPosition(left + len(text), c.KeepAnchor) self.setTextCursor(c) def insert_hyperlink(self, target, text): if hasattr(self.smarts, 'insert_hyperlink'): self.smarts.insert_hyperlink(self, target, text) def insert_tag(self, tag): if hasattr(self.smarts, 'insert_tag'): self.smarts.insert_tag(self, tag) def keyPressEvent(self, ev): if ev.key() == Qt.Key_X and ev.modifiers() == Qt.AltModifier: if self.replace_possible_unicode_sequence(): ev.accept() return if ev.key() == Qt.Key_Insert: self.setOverwriteMode(self.overwriteMode() ^ True) ev.accept() return if self.snippet_manager.handle_key_press(ev): self.completion_popup.hide() return if self.smarts.handle_key_press(ev, self): self.handle_keypress_completion(ev) return QPlainTextEdit.keyPressEvent(self, ev) self.handle_keypress_completion(ev) def handle_keypress_completion(self, ev): if self.request_completion is None: return code = ev.key() if code in (0, Qt.Key_unknown, Qt.Key_Shift, Qt.Key_Control, Qt.Key_Alt, Qt.Key_Meta, Qt.Key_AltGr, Qt.Key_CapsLock, Qt.Key_NumLock, Qt.Key_ScrollLock, Qt.Key_Up, Qt.Key_Down): # We ignore up/down arrow so as to not break scrolling through the # text with the arrow keys return result = self.smarts.get_completion_data(self, ev) if result is None: self.last_completion_request += 1 else: self.last_completion_request = self.request_completion(*result) self.completion_popup.mark_completion( self, None if result is None else result[-1]) def handle_completion_result(self, result): if result.request_id[0] >= self.last_completion_request: self.completion_popup.handle_result(result) def clear_completion_cache(self): if self.request_completion is not None and self.completion_doc_name: self.request_completion(None, 'file:' + self.completion_doc_name) def replace_possible_unicode_sequence(self): c = self.textCursor() has_selection = c.hasSelection() if has_selection: text = unicode(c.selectedText()).rstrip('\0') else: c.setPosition(c.position() - min(c.positionInBlock(), 6), c.KeepAnchor) text = unicode(c.selectedText()).rstrip('\0') m = re.search(r'[a-fA-F0-9]{2,6}$', text) if m is None: return False text = m.group() try: num = int(text, 16) except ValueError: return False if num > 0x10ffff or num < 1: return False end_pos = max(c.anchor(), c.position()) c.setPosition(end_pos - len(text)), c.setPosition( end_pos, c.KeepAnchor) c.insertText(safe_chr(num)) return True def select_all(self): c = self.textCursor() c.clearSelection() c.setPosition(0) c.movePosition(c.End, c.KeepAnchor) self.setTextCursor(c) def rename_block_tag(self, new_name): if hasattr(self.smarts, 'rename_block_tag'): self.smarts.rename_block_tag(self, new_name) def current_tag(self, for_position_sync=True): return self.smarts.cursor_position_with_sourceline( self.textCursor(), for_position_sync=for_position_sync) def goto_sourceline(self, sourceline, tags, attribute=None): return self.smarts.goto_sourceline(self, sourceline, tags, attribute=attribute) def get_tag_contents(self): c = self.smarts.get_inner_HTML(self) if c is not None: return self.selected_text_from_cursor(c) def goto_css_rule(self, rule_address, sourceline_address=None): from calibre.gui2.tweak_book.editor.smarts.css import find_rule block = None if self.syntax == 'css': raw = unicode(self.toPlainText()) line, col = find_rule(raw, rule_address) if line is not None: block = self.document().findBlockByNumber(line - 1) elif sourceline_address is not None: sourceline, tags = sourceline_address if self.goto_sourceline(sourceline, tags): c = self.textCursor() c.setPosition(c.position() + 1) self.setTextCursor(c) raw = self.get_tag_contents() line, col = find_rule(raw, rule_address) if line is not None: block = self.document().findBlockByNumber(c.blockNumber() + line - 1) if block is not None and block.isValid(): c = self.textCursor() c.setPosition(block.position() + col) self.setTextCursor(c) def change_case(self, action, cursor=None): cursor = cursor or self.textCursor() text = self.selected_text_from_cursor(cursor) text = { 'lower': lower, 'upper': upper, 'capitalize': capitalize, 'title': titlecase, 'swap': swapcase }[action](text) cursor.insertText(text) self.setTextCursor(cursor)
def __init__(self, mygui, myguidb, mymainprefs, myparam_dict, myuiexit, mysavedialoggeometry): super(LibraryCodesTab, self).__init__() #----------------------------------------------------- #----------------------------------------------------- self.gui = mygui #----------------------------------------------------- #----------------------------------------------------- self.guidb = myguidb #----------------------------------------------------- #----------------------------------------------------- self.lib_path = self.gui.library_view.model().db.library_path #----------------------------------------------------- #----------------------------------------------------- self.mytabprefs = mymainprefs #----------------------------------------------------- #----------------------------------------------------- self.param_dict = myparam_dict #----------------------------------------------------- #----------------------------------------------------- self.ui_exit = myuiexit #----------------------------------------------------- #----------------------------------------------------- self.save_dialog_geometry = mysavedialoggeometry #----------------------------------------------------- #----------------------------------------------------- font = QFont() font.setBold(False) font.setPointSize(10) #----------------------------------------------------- self.layout_top = QVBoxLayout() self.layout_top.setSpacing(0) self.layout_top.setAlignment(Qt.AlignLeft) self.setLayout(self.layout_top) #----------------------------------------------------- self.scroll_area_frame = QScrollArea() self.scroll_area_frame.setAlignment(Qt.AlignLeft) self.scroll_area_frame.setWidgetResizable(True) self.scroll_area_frame.ensureVisible(400, 400) self.layout_top.addWidget( self.scroll_area_frame ) # the scroll area is now the child of the parent of self.layout_top # NOTE: the self.scroll_area_frame.setWidget(self.scroll_widget) is at the end of the init() AFTER all children have been created and assigned to a layout... #----------------------------------------------------- self.scroll_widget = QWidget() self.layout_top.addWidget( self.scroll_widget ) # causes automatic reparenting of QWidget to the parent of self.layout_top, which is: self . #----------------------------------------------------- self.layout_frame = QVBoxLayout() self.layout_frame.setSpacing(0) self.layout_frame.setAlignment(Qt.AlignLeft) self.scroll_widget.setLayout( self.layout_frame ) # causes automatic reparenting of any widget later added to self.layout_frame to the parent of self.layout_frame, which is: QWidget . #----------------------------------------------------- self.lc_groupbox = QGroupBox('Settings:') self.lc_groupbox.setMaximumWidth(400) self.lc_groupbox.setToolTip( "<p style='white-space:wrap'>The settings that control 'Library Codes'. Using only ISBN or ISSN or Author/Title, Library Codes for selected books will be derived using the Current Settings." ) self.layout_frame.addWidget(self.lc_groupbox) self.lc_layout = QGridLayout() self.lc_groupbox.setLayout(self.lc_layout) #----------------------------------------------------- #----------------------------------------------------- #----------------------------------------------------- #----------------------------------------------------- self.spacing0 = QLabel() self.layout_frame.addWidget(self.spacing0) self.spacing0.setMaximumHeight(20) #----------------------------------------------------- #----------------------------------------------------- #----------------------------------------------------- self.button_box = QDialogButtonBox() self.button_box.setOrientation(Qt.Horizontal) self.button_box.setCenterButtons(True) self.layout_frame.addWidget(self.button_box) #----------------------------------------------------- #----------------------------------------------------- #----------------------------------------------------- #----------------------------------------------------- #----------------------------------------------------- #----------------------------------------------------- self.push_button_save_only = QPushButton("Save") self.push_button_save_only.clicked.connect(self.save_settings) self.push_button_save_only.setDefault(True) self.push_button_save_only.setFont(font) self.push_button_save_only.setToolTip( "<p style='white-space:wrap'>Save all user settings.") self.button_box.addButton(self.push_button_save_only, 0) self.push_button_exit_only = QPushButton("Exit") self.push_button_exit_only.clicked.connect(self.exit_only) self.push_button_exit_only.setDefault(False) self.push_button_exit_only.setFont(font) self.push_button_exit_only.setToolTip( "<p style='white-space:wrap'>Exit immediately without saving anything." ) self.button_box.addButton(self.push_button_exit_only, 0) #----------------------------------------------------- #----------------------------------------------------- #----------------------------------------------------- #----------------------------------------------------- r = 4 self.ddc_labelname = QLineEdit(self) self.ddc_labelname.setText(self.mytabprefs['DDC']) self.ddc_labelname.setFont(font) self.ddc_labelname.setToolTip( "<p style='white-space:wrap'>Custom Column Search/Lookup #name for DDC.<br><br>See: https://www.oclc.org/dewey/features/summaries.en.html" ) self.ddc_labelname.setMaximumWidth(100) self.lc_layout.addWidget(self.ddc_labelname, r, 0) self.ddc_activate_checkbox = QCheckBox( "Activate 'Dewey Decimal Code' Classification?") self.ddc_activate_checkbox.setToolTip( "<p style='white-space:wrap'>Do you want to derive DDC?") r = r + 1 self.lc_layout.addWidget(self.ddc_activate_checkbox, r, 0) if prefs['DDC_IS_ACTIVE'] == unicode_type(S_TRUE): self.ddc_activate_checkbox.setChecked(True) else: self.ddc_activate_checkbox.setChecked(False) #----------------------------------------------------- self.spacing1 = QLabel() r = r + 1 self.lc_layout.addWidget(self.spacing1, r, 0) self.spacing1.setMaximumHeight(10) #----------------------------------------------------- self.lcc_labelname = QLineEdit(self) self.lcc_labelname.setText(self.mytabprefs['LCC']) self.lcc_labelname.setFont(font) self.lcc_labelname.setToolTip( "<p style='white-space:wrap'>Custom Column Search/Lookup #name for LCC.<br><br>See: http://www.loc.gov/catdir/cpso/lcco/ " ) self.lcc_labelname.setMaximumWidth(100) r = r + 4 self.lc_layout.addWidget(self.lcc_labelname, r, 0) self.lcc_activate_checkbox = QCheckBox( "Activate 'Library of Congress Code' Classification?") self.lcc_activate_checkbox.setToolTip( "<p style='white-space:wrap'>Do you want to derive LCC?") r = r + 1 self.lc_layout.addWidget(self.lcc_activate_checkbox, r, 0) if prefs['LCC_IS_ACTIVE'] == unicode_type(S_TRUE): self.lcc_activate_checkbox.setChecked(True) else: self.lcc_activate_checkbox.setChecked(False) #----------------------------------------------------- self.spacing2 = QLabel("") r = r + 1 self.lc_layout.addWidget(self.spacing2, r, 0) self.spacing2.setMaximumHeight(10) #----------------------------------------------------- self.fast_labelname = QLineEdit(self) self.fast_labelname.setText(self.mytabprefs['FAST']) self.fast_labelname.setFont(font) self.fast_labelname.setToolTip( "<p style='white-space:wrap'>Custom Column Search/Lookup #name for FAST Tag Values. " ) self.fast_labelname.setMinimumWidth(100) self.fast_labelname.setMaximumWidth(100) r = r + 4 self.lc_layout.addWidget(self.fast_labelname, r, 0) self.fast_activate_checkbox = QCheckBox("Activate 'FAST' Tags?") self.fast_activate_checkbox.setToolTip( "<p style='white-space:wrap'>Do you want to derive FAST Tags?\ <br><br>Text. Behaves like Tags. Not Names.<br><br>" ) r = r + 1 self.lc_layout.addWidget(self.fast_activate_checkbox, r, 0) if prefs['FAST_IS_ACTIVE'] == unicode_type(S_TRUE): self.fast_activate_checkbox.setChecked(True) else: self.fast_activate_checkbox.setChecked(False) #----------------------------------------------------- self.spacing6 = QLabel("") r = r + 1 self.lc_layout.addWidget(self.spacing6, r, 0) self.spacing6.setMaximumHeight(10) #----------------------------------------------------- self.oclc_labelname = QLineEdit(self) self.oclc_labelname.setText(self.mytabprefs['OCLC']) self.oclc_labelname.setFont(font) self.oclc_labelname.setToolTip( "<p style='white-space:wrap'>Custom Column Search/Lookup #name for OCLC-OWI.<br><br>See: #http://classify.oclc.org/classify2/ " ) self.oclc_labelname.setMaximumWidth(100) r = r + 4 self.lc_layout.addWidget(self.oclc_labelname, r, 0) self.oclc_activate_checkbox = QCheckBox( "Activate 'Online Computer Library Center' Work ID Code?") self.oclc_activate_checkbox.setToolTip( "<p style='white-space:wrap'>Do you want to derive OCLC-OWI?") r = r + 1 self.lc_layout.addWidget(self.oclc_activate_checkbox, r, 0) if self.mytabprefs['OCLC_IS_ACTIVE'] == unicode_type(S_TRUE): self.oclc_activate_checkbox.setChecked(True) else: self.oclc_activate_checkbox.setChecked(False) #----------------------------------------------------- self.spacing5 = QLabel("") r = r + 1 self.lc_layout.addWidget(self.spacing5, r, 0) self.spacing5.setMaximumHeight(10) #----------------------------------------------------- self.lc_author_details_labelname = QLineEdit(self) self.lc_author_details_labelname.setText( self.mytabprefs['EXTRA_AUTHOR_DETAILS']) self.lc_author_details_labelname.setFont(font) self.lc_author_details_labelname.setToolTip( "<p style='white-space:wrap'>Custom Column Search/Lookup #name for 'LC Extra Author Details'.\ <br><br>Text. Behaves like Tags. Not Names.<br><br>" ) self.lc_author_details_labelname.setMaximumWidth(100) r = r + 4 self.lc_layout.addWidget(self.lc_author_details_labelname, r, 0) self.lc_author_details_checkbox = QCheckBox( "Activate 'Library Codes Extra Author Details'?") self.lc_author_details_checkbox.setToolTip( "<p style='white-space:wrap'>Do you want to add (never delete or replace) any available Tag-like values to this Custom Column if they are associated with the OCLC-OWI Identifier?" ) r = r + 1 self.lc_layout.addWidget(self.lc_author_details_checkbox, r, 0) if self.mytabprefs['EXTRA_AUTHOR_DETAILS_IS_ACTIVE'] == unicode_type( S_TRUE): self.lc_author_details_checkbox.setChecked(True) else: self.lc_author_details_checkbox.setChecked(False) #----------------------------------------------------- #----------------------------------------------------- self.spacing4 = QLabel() r = r + 1 self.lc_layout.addWidget(self.spacing4, r, 0) self.spacing4.setMaximumHeight(10) #----------------------------------------------------- font.setBold(False) font.setPointSize(7) #----------------------------------------------------- self.push_button_autoadd_custom_columns = QPushButton( "Automatically Add Activated Custom Columns?") self.push_button_autoadd_custom_columns.clicked.connect( self.autoadd_custom_columns) self.push_button_autoadd_custom_columns.setDefault(False) self.push_button_autoadd_custom_columns.setFont(font) self.push_button_autoadd_custom_columns.setToolTip( "<p style='white-space:wrap'>Do you want to automatically add the Custom Columns selected above?<br><br>If you have any issues, please add them manually." ) r = r + 4 self.lc_layout.addWidget(self.push_button_autoadd_custom_columns, r, 0) self.push_button_autoadd_custom_columns.setMaximumWidth(250) #----------------------------------------------------- self.lc_custom_columns_generation_label = QLabel() r = r + 1 self.lc_layout.addWidget(self.lc_custom_columns_generation_label, r, 0) self.lc_custom_columns_generation_label.setText( " ") self.lc_custom_columns_generation_label.setMaximumHeight(10) self.lc_custom_columns_generation_label.setFont(font) self.oclc_identifier_only_checkbox = QCheckBox( "Always Create OCLC-OWI as an 'Identifier' (à la ISBN)?") self.oclc_identifier_only_checkbox.setToolTip( "<p style='white-space:wrap'>Do you want to update Calibre's Identifiers for an Identifier of 'OCLC-OWI',\ regardless of whether you want its own Custom Column updated?\ <br><br>REQUIRED to derive DDC/LCC using Author/Title." ) r = r + 2 self.lc_layout.addWidget(self.oclc_identifier_only_checkbox, r, 0) if prefs['OCLC_IDENTIFIER'] == unicode_type(S_TRUE): self.oclc_identifier_only_checkbox.setChecked(True) else: self.oclc_identifier_only_checkbox.setChecked(False) #----------------------------------------------------- self.spacing3 = QLabel("") r = r + 1 self.lc_layout.addWidget(self.spacing3, r, 0) self.spacing3.setMaximumHeight(10) #----------------------------------------------------- font.setBold(False) font.setPointSize(10) #----------------------------------------------------- self.lc_genre_labelname = QLineEdit(self) self.lc_genre_labelname.setText(self.mytabprefs['GENRE']) self.lc_genre_labelname.setFont(font) self.lc_genre_labelname.setToolTip( "<p style='white-space:wrap'>Custom Column Search/Lookup #name for 'Genre'.\ <br><br>Text. Behaves like Tags.<br><br>" ) self.lc_genre_labelname.setMaximumWidth(100) r = r + 1 self.lc_layout.addWidget(self.lc_genre_labelname, r, 0) self.lc_checkbox_buttongroup = QButtonGroup() self.lc_checkbox_buttongroup.setExclusive(True) self.lc_genre_ddc_checkbox = QCheckBox( "Update 'Genre' using DDC-to-Genre Mappings?") self.lc_genre_ddc_checkbox.setToolTip( "<p style='white-space:wrap'>Do you want LC to update 'Genre' using the DDC-to-Genre mapping in Table _lc_genre_mapping?" ) r = r + 1 self.lc_layout.addWidget(self.lc_genre_ddc_checkbox, r, 0) self.lc_genre_lcc_checkbox = QCheckBox( "Update 'Genre' using LCC-to-Genre Mappings?") self.lc_genre_lcc_checkbox.setToolTip( "<p style='white-space:wrap'>Do you want LC to update 'Genre' using the LCC-to-Genre mapping in Table _lc_genre_mapping?" ) r = r + 1 self.lc_layout.addWidget(self.lc_genre_lcc_checkbox, r, 0) self.lc_genre_inactive_checkbox = QCheckBox( "Do not update 'Genre' at all") self.lc_genre_inactive_checkbox.setToolTip( "<p style='white-space:wrap'>Do no 'Genre' processing at all?") r = r + 1 self.lc_layout.addWidget(self.lc_genre_inactive_checkbox, r, 0) self.lc_checkbox_buttongroup.addButton(self.lc_genre_ddc_checkbox) self.lc_checkbox_buttongroup.addButton(self.lc_genre_lcc_checkbox) self.lc_checkbox_buttongroup.addButton(self.lc_genre_inactive_checkbox) if self.mytabprefs['GENRE_DDC_IS_ACTIVE'] == unicode_type(S_TRUE): self.lc_genre_ddc_checkbox.setChecked(True) elif self.mytabprefs['GENRE_LCC_IS_ACTIVE'] == unicode_type(S_TRUE): self.lc_genre_lcc_checkbox.setChecked(True) elif self.mytabprefs['GENRE_IS_INACTIVE'] == unicode_type(S_TRUE): self.lc_genre_inactive_checkbox.setChecked(True) self.lc_exact_match_checkbox = QCheckBox( "DDC: Require an 'Exact Match', not a 'Best Match'?") self.lc_exact_match_checkbox.setToolTip( "<p style='white-space:wrap'>Check this checkbox if you want an exact DDC match to be required in Table _lc_genre_mapping. Otherwise, a 'best match' will be used via progressive shortening from right to left, but not past any decimal point. If there is no decimal point in a book's DDC, then no progressive shortening will be performed at all." ) r = r + 1 self.lc_layout.addWidget(self.lc_exact_match_checkbox, r, 0) if self.mytabprefs['GENRE_EXACT_MATCH'] == unicode_type(S_TRUE): self.lc_exact_match_checkbox.setChecked(True) self.spin_lcc = QSpinBox(self) self.spin_lcc.setMinimum(1) self.spin_lcc.setMaximum(50) self.spin_lcc.setProperty('value', prefs['GENRE_LCC_MATCH_LENGTH']) self.spin_lcc.setMaximumWidth(250) self.spin_lcc.setSuffix(" LCC: Maximum Length to Match") self.spin_lcc.setToolTip( "<p style='white-space:nowrap'>Maximum number of characters in the LCC that should be used to map to the 'Genre', starting from the left. A maximum of 1 guarantees a (broad) match.\ <br><br>LCCs are structured with either 1 or 2 beginning letters, so 2-character LCCs have special matching logic.\ <br><br>Example: Assume maximum = 2 for a LCC of 'Q1': Q1 would be attempted. If it failed, because the 2nd digit is a number, 'Q' would be attempted.\ <br><br>Example: Assume maximum = 2 for a LCC of 'PN1969.C65': PN would be attempted. If it failed, nothing else would be attempted.\ <br><br>Example: Assume maximum = 4 for a LCC of 'PN1969.C65': PN19 would be attempted. If it failed, nothing else would be attempted.\ <br><br>Example: Assume maximum = 4 for a LCC of 'Q1': Q1 would be attempted. If it failed, because the 2nd digit is a number, 'Q' would be attempted.\ <br><br>Example: Assume maximum = 4 for a LCC of 'Q389': Q389 would be attempted. If it failed, nothing else would be attempted." ) r = r + 2 self.lc_layout.addWidget(self.spin_lcc, r, 0) #----------------------------------------------------- #----------------------------------------------------- #----------------------------------------------------- #----------------------------------------------------- #----------------------------------------------------- #----------------------------------------------------- self.scroll_widget.resize(self.sizeHint()) #----------------------------------------------------- #----------------------------------------------------- self.scroll_area_frame.setWidget( self.scroll_widget ) # now that all widgets have been created and assigned to a layout... #----------------------------------------------------- #----------------------------------------------------- self.scroll_area_frame.resize(self.sizeHint()) #----------------------------------------------------- #----------------------------------------------------- self.resize(self.sizeHint())
def __init__(self, args, force_calibre_style=False, override_program_name=None, headless=False, color_prefs=gprefs): self.file_event_hook = None if override_program_name: args = [override_program_name] + args[1:] if headless: if not args: args = sys.argv[:1] args.extend(['-platformpluginpath', sys.extensions_location, '-platform', 'headless']) self.headless = headless qargs = [i.encode('utf-8') if isinstance(i, unicode) else i for i in args] self.pi = plugins['progress_indicator'][0] QApplication.__init__(self, qargs) if islinux or isbsd: self.setAttribute(Qt.AA_DontUseNativeMenuBar, 'CALIBRE_NO_NATIVE_MENUBAR' in os.environ) self.setup_styles(force_calibre_style) f = QFont(QApplication.font()) if (f.family(), f.pointSize()) == ('Sans Serif', 9): # Hard coded Qt settings, no user preference detected f.setPointSize(10) QApplication.setFont(f) f = QFontInfo(f) self.original_font = (f.family(), f.pointSize(), f.weight(), f.italic(), 100) if not self.using_calibre_style and self.style().objectName() == 'fusion': # Since Qt is using the fusion style anyway, specialize it self.load_calibre_style() fi = gprefs['font'] if fi is not None: font = QFont(*(fi[:4])) s = gprefs.get('font_stretch', None) if s is not None: font.setStretch(s) QApplication.setFont(font) dl = QLocale(get_lang()) if unicode(dl.bcp47Name()) != u'C': QLocale.setDefault(dl) global gui_thread, qt_app gui_thread = QThread.currentThread() self._translator = None self.load_translations() qt_app = self self._file_open_paths = [] self._file_open_lock = RLock() if not isosx: # OS X uses a native color dialog that does not support custom # colors self.color_prefs = color_prefs self.read_custom_colors() self.lastWindowClosed.connect(self.save_custom_colors) if isxp: error_dialog(None, _('Windows XP not supported'), '<p>' + _( 'calibre versions newer than 2.0 do not run on Windows XP. This is' ' because the graphics toolkit calibre uses (Qt 5) crashes a lot' ' on Windows XP. We suggest you stay with <a href="%s">calibre 1.48</a>' ' which works well on Windows XP.') % 'http://download.calibre-ebook.com/1.48.0/', show=True) raise SystemExit(1) if iswindows: # On windows the highlighted colors for inactive widgets are the # same as non highlighted colors. This is a regression from Qt 4. # https://bugreports.qt-project.org/browse/QTBUG-41060 p = self.palette() for role in (p.Highlight, p.HighlightedText, p.Base, p.AlternateBase): p.setColor(p.Inactive, role, p.color(p.Active, role)) self.setPalette(p) # Prevent text copied to the clipboard from being lost on quit due to # Qt 5 bug: https://bugreports.qt-project.org/browse/QTBUG-41125 self.aboutToQuit.connect(self.flush_clipboard)
def __init__(self, gui, icon, guidb, plugin_path, ui_exit, action_type): parent = gui unique_pref_name = 'library_codes:gui_parameters_dialog' SizePersistedDialog.__init__(self, parent, unique_pref_name) #----------------------------------------------------- self.gui = gui self.guidb = guidb #----------------------------------------------------- self.icon = icon #----------------------------------------------------- self.plugin_path = plugin_path #----------------------------------------------------- self.ui_exit = ui_exit #----------------------------------------------------- self.action_type = action_type #----------------------------------------------------- self.myparentprefs = collections.OrderedDict([]) prefsdefaults = deepcopy(prefs.defaults) tmp_list = [] #~ for k,v in prefs.iteritems(): for k, v in iteritems(prefs): tmp_list.append(k) #END FOR #~ for k,v in prefsdefaults.iteritems(): for k, v in iteritems(prefsdefaults): tmp_list.append(k) #END FOR tmp_set = set(tmp_list) tmp_list = list(tmp_set) #no duplicates del tmp_set tmp_list.sort() for k in tmp_list: self.myparentprefs[k] = " " # ordered by key #END FOR del tmp_list #~ for k,v in prefs.iteritems(): for k, v in iteritems(prefs): self.myparentprefs[k] = v #END FOR #~ for k,v in prefsdefaults.iteritems(): for k, v in iteritems(prefsdefaults): if not k in prefs: prefs[k] = v else: if not prefs[k] > " ": prefs[k] = v if not k in self.myparentprefs: self.myparentprefs[k] = v else: if not self.myparentprefs[k] > " ": self.myparentprefs[k] = v #END FOR #~ for k,v in self.myparentprefs.iteritems(): for k, v in iteritems(self.myparentprefs): prefs[k] = v #END FOR prefs #prefs now synched #----------------------------------------------------- #----------------------------------------------------- #----------------------------------------------------- #----------------------------------------------------- self.param_dict = collections.OrderedDict([]) #----------------------------------------------------- #----------------------------------------------------- #----------------------------------------------------- self.init_tooltips_for_parent() self.setToolTip(self.parent_tooltip) #----------------------------------------------------- #----------------------------------------------------- #----------------------------------------------------- #----------------------------------------------------- # Tab 0: LibraryCodesTab #----------------------------------------------------- #----------------------------------------------------- from calibre_plugins.library_codes.library_codes_dialog import LibraryCodesTab self.LibraryCodesTab = LibraryCodesTab(self.gui, self.guidb, self.myparentprefs, self.param_dict, self.ui_exit, self.save_dialog_geometry) #----------------------------------------------------- #----------------------------------------------------- #----------------------------------------------------- # Parent LibraryCodesDialog #----------------------------------------------------- font = QFont() font.setBold(False) font.setPointSize(10) tablabel_font = QFont() tablabel_font.setBold(False) tablabel_font.setPointSize(10) #----------------------------------------------------- self.setWindowTitle('Library Codes') self.setWindowIcon(icon) #----------------------------------------------------- self.layout_frame = QVBoxLayout() self.layout_frame.setAlignment(Qt.AlignLeft) self.setLayout(self.layout_frame) #----------------------------------------------------- #----------------------------------------------------- #----------------------------------------------------- n_width = 600 self.LCtabWidget = QTabWidget() self.LCtabWidget.setMaximumWidth(n_width) self.LCtabWidget.setFont(tablabel_font) self.LCtabWidget.addTab( self.LibraryCodesTab, "Derivation from ISBN or ISSN or Author/Title") self.LibraryCodesTab.setToolTip( "<p style='white-space:wrap'>Derive Library Codes DDC and/or LCC and/or OCLC-OWI from ISBN or ISSN or Author/Title. Visit: http://classify.oclc.org/classify2/ " ) self.LibraryCodesTab.setMaximumWidth(n_width) #----------------------------------------------------- self.layout_frame.addWidget(self.LCtabWidget) #----------------------------------------------------- #----------------------------------------------------- #----------------------------------------------------- #----------------------------------------------------- self.resize_dialog() # inherited from SizePersistedDialog #----------------------------------------------------- self.LCtabWidget.setCurrentIndex(0)
class TextEdit(PlainTextEdit): link_clicked = pyqtSignal(object) smart_highlighting_updated = pyqtSignal() def __init__(self, parent=None, expected_geometry=(100, 50)): PlainTextEdit.__init__(self, parent) self.gutter_width = 0 self.expected_geometry = expected_geometry self.saved_matches = {} self.smarts = NullSmarts(self) self.current_cursor_line = None self.current_search_mark = None self.smarts_highlight_timer = t = QTimer() t.setInterval(750), t.setSingleShot(True), t.timeout.connect(self.update_extra_selections) self.highlighter = SyntaxHighlighter() self.line_number_area = LineNumbers(self) self.apply_settings() self.setMouseTracking(True) self.cursorPositionChanged.connect(self.highlight_cursor_line) self.blockCountChanged[int].connect(self.update_line_number_area_width) self.updateRequest.connect(self.update_line_number_area) self.syntax = None @dynamic_property def is_modified(self): ''' True if the document has been modified since it was loaded or since the last time is_modified was set to False. ''' def fget(self): return self.document().isModified() def fset(self, val): self.document().setModified(bool(val)) return property(fget=fget, fset=fset) def sizeHint(self): return self.size_hint def apply_settings(self, prefs=None, dictionaries_changed=False): # {{{ prefs = prefs or tprefs self.setLineWrapMode(QPlainTextEdit.WidgetWidth if prefs['editor_line_wrap'] else QPlainTextEdit.NoWrap) theme = get_theme(prefs['editor_theme']) self.apply_theme(theme) w = self.fontMetrics() self.space_width = w.width(' ') self.setTabStopWidth(prefs['editor_tab_stop_width'] * self.space_width) if dictionaries_changed: self.highlighter.rehighlight() def apply_theme(self, theme): self.theme = theme pal = self.palette() pal.setColor(pal.Base, theme_color(theme, 'Normal', 'bg')) pal.setColor(pal.AlternateBase, theme_color(theme, 'CursorLine', 'bg')) pal.setColor(pal.Text, theme_color(theme, 'Normal', 'fg')) pal.setColor(pal.Highlight, theme_color(theme, 'Visual', 'bg')) pal.setColor(pal.HighlightedText, theme_color(theme, 'Visual', 'fg')) self.setPalette(pal) self.tooltip_palette = pal = QPalette() pal.setColor(pal.ToolTipBase, theme_color(theme, 'Tooltip', 'bg')) pal.setColor(pal.ToolTipText, theme_color(theme, 'Tooltip', 'fg')) self.line_number_palette = pal = QPalette() pal.setColor(pal.Base, theme_color(theme, 'LineNr', 'bg')) pal.setColor(pal.Text, theme_color(theme, 'LineNr', 'fg')) pal.setColor(pal.BrightText, theme_color(theme, 'LineNrC', 'fg')) self.match_paren_format = theme_format(theme, 'MatchParen') font = self.font() ff = tprefs['editor_font_family'] if ff is None: ff = default_font_family() font.setFamily(ff) font.setPointSize(tprefs['editor_font_size']) self.tooltip_font = QFont(font) self.tooltip_font.setPointSize(font.pointSize() - 1) self.setFont(font) self.highlighter.apply_theme(theme) w = self.fontMetrics() self.number_width = max(map(lambda x:w.width(str(x)), xrange(10))) self.size_hint = QSize(self.expected_geometry[0] * w.averageCharWidth(), self.expected_geometry[1] * w.height()) self.highlight_color = theme_color(theme, 'HighlightRegion', 'bg') self.highlight_cursor_line() # }}} def load_text(self, text, syntax='html', process_template=False, doc_name=None): self.syntax = syntax self.highlighter = get_highlighter(syntax)() self.highlighter.apply_theme(self.theme) self.highlighter.set_document(self.document(), doc_name=doc_name) sclass = {'html':HTMLSmarts, 'xml':HTMLSmarts, 'css':CSSSmarts}.get(syntax, None) if sclass is not None: self.smarts = sclass(self) self.setPlainText(unicodedata.normalize('NFC', text)) if process_template and QPlainTextEdit.find(self, '%CURSOR%'): c = self.textCursor() c.insertText('') def change_document_name(self, newname): self.highlighter.doc_name = newname self.highlighter.rehighlight() # Ensure links are checked w.r.t. the new name correctly def replace_text(self, text): c = self.textCursor() pos = c.position() c.beginEditBlock() c.clearSelection() c.select(c.Document) c.insertText(unicodedata.normalize('NFC', text)) c.endEditBlock() c.setPosition(min(pos, len(text))) self.setTextCursor(c) self.ensureCursorVisible() def simple_replace(self, text): c = self.textCursor() c.insertText(unicodedata.normalize('NFC', text)) self.setTextCursor(c) def go_to_line(self, lnum, col=None): lnum = max(1, min(self.blockCount(), lnum)) c = self.textCursor() c.clearSelection() c.movePosition(c.Start) c.movePosition(c.NextBlock, n=lnum - 1) c.movePosition(c.StartOfLine) c.movePosition(c.EndOfLine, c.KeepAnchor) text = unicode(c.selectedText()).rstrip('\0') if col is None: c.movePosition(c.StartOfLine) lt = text.lstrip() if text and lt and lt != text: c.movePosition(c.NextWord) else: c.setPosition(c.block().position() + col) if c.blockNumber() + 1 > lnum: # We have moved past the end of the line c.setPosition(c.block().position()) c.movePosition(c.EndOfBlock) self.setTextCursor(c) self.ensureCursorVisible() def update_extra_selections(self, instant=True): sel = [] if self.current_cursor_line is not None: sel.append(self.current_cursor_line) if self.current_search_mark is not None: sel.append(self.current_search_mark) if instant and not self.highlighter.has_requests: sel.extend(self.smarts.get_extra_selections(self)) self.smart_highlighting_updated.emit() else: self.smarts_highlight_timer.start() self.setExtraSelections(sel) # Search and replace {{{ def mark_selected_text(self): sel = QTextEdit.ExtraSelection() sel.format.setBackground(self.highlight_color) sel.cursor = self.textCursor() if sel.cursor.hasSelection(): self.current_search_mark = sel c = self.textCursor() c.clearSelection() self.setTextCursor(c) else: self.current_search_mark = None self.update_extra_selections() def find_in_marked(self, pat, wrap=False, save_match=None): if self.current_search_mark is None: return False csm = self.current_search_mark.cursor reverse = pat.flags & regex.REVERSE c = self.textCursor() c.clearSelection() m_start = min(csm.position(), csm.anchor()) m_end = max(csm.position(), csm.anchor()) if c.position() < m_start: c.setPosition(m_start) if c.position() > m_end: c.setPosition(m_end) pos = m_start if reverse else m_end if wrap: pos = m_end if reverse else m_start c.setPosition(pos, c.KeepAnchor) raw = unicode(c.selectedText()).replace(PARAGRAPH_SEPARATOR, '\n').rstrip('\0') m = pat.search(raw) if m is None: return False start, end = m.span() if start == end: return False if wrap: if reverse: textpos = c.anchor() start, end = textpos + end, textpos + start else: start, end = m_start + start, m_start + end else: if reverse: start, end = m_start + end, m_start + start else: start, end = c.anchor() + start, c.anchor() + end c.clearSelection() c.setPosition(start) c.setPosition(end, c.KeepAnchor) self.setTextCursor(c) # Center search result on screen self.centerCursor() if save_match is not None: self.saved_matches[save_match] = (pat, m) return True def all_in_marked(self, pat, template=None): if self.current_search_mark is None: return 0 c = self.current_search_mark.cursor raw = unicode(c.selectedText()).replace(PARAGRAPH_SEPARATOR, '\n').rstrip('\0') if template is None: count = len(pat.findall(raw)) else: raw, count = pat.subn(template, raw) if count > 0: start_pos = min(c.anchor(), c.position()) c.insertText(raw) end_pos = max(c.anchor(), c.position()) c.setPosition(start_pos), c.setPosition(end_pos, c.KeepAnchor) self.update_extra_selections() return count def find(self, pat, wrap=False, marked=False, complete=False, save_match=None): if marked: return self.find_in_marked(pat, wrap=wrap, save_match=save_match) reverse = pat.flags & regex.REVERSE c = self.textCursor() c.clearSelection() if complete: # Search the entire text c.movePosition(c.End if reverse else c.Start) pos = c.Start if reverse else c.End if wrap and not complete: pos = c.End if reverse else c.Start c.movePosition(pos, c.KeepAnchor) raw = unicode(c.selectedText()).replace(PARAGRAPH_SEPARATOR, '\n').rstrip('\0') m = pat.search(raw) if m is None: return False start, end = m.span() if start == end: return False if wrap and not complete: if reverse: textpos = c.anchor() start, end = textpos + end, textpos + start else: if reverse: # Put the cursor at the start of the match start, end = end, start else: textpos = c.anchor() start, end = textpos + start, textpos + end c.clearSelection() c.setPosition(start) c.setPosition(end, c.KeepAnchor) self.setTextCursor(c) # Center search result on screen self.centerCursor() if save_match is not None: self.saved_matches[save_match] = (pat, m) return True def find_spell_word(self, original_words, lang, from_cursor=True, center_on_cursor=True): c = self.textCursor() c.setPosition(c.position()) if not from_cursor: c.movePosition(c.Start) c.movePosition(c.End, c.KeepAnchor) def find_first_word(haystack): match_pos, match_word = -1, None for w in original_words: idx = index_of(w, haystack, lang=lang) if idx > -1 and (match_pos == -1 or match_pos > idx): match_pos, match_word = idx, w return match_pos, match_word while True: text = unicode(c.selectedText()).rstrip('\0') idx, word = find_first_word(text) if idx == -1: return False c.setPosition(c.anchor() + idx) c.setPosition(c.position() + string_length(word), c.KeepAnchor) if self.smarts.verify_for_spellcheck(c, self.highlighter): self.setTextCursor(c) if center_on_cursor: self.centerCursor() return True c.setPosition(c.position()) c.movePosition(c.End, c.KeepAnchor) return False def find_next_spell_error(self, from_cursor=True): c = self.textCursor() if not from_cursor: c.movePosition(c.Start) block = c.block() while block.isValid(): for r in block.layout().additionalFormats(): if r.format.property(SPELL_PROPERTY): if not from_cursor or block.position() + r.start + r.length > c.position(): c.setPosition(block.position() + r.start) c.setPosition(c.position() + r.length, c.KeepAnchor) self.setTextCursor(c) return True block = block.next() return False def replace(self, pat, template, saved_match='gui'): c = self.textCursor() raw = unicode(c.selectedText()).replace(PARAGRAPH_SEPARATOR, '\n').rstrip('\0') m = pat.fullmatch(raw) if m is None: # This can happen if either the user changed the selected text or # the search expression uses lookahead/lookbehind operators. See if # the saved match matches the currently selected text and # use it, if so. if saved_match is not None and saved_match in self.saved_matches: saved_pat, saved = self.saved_matches.pop(saved_match) if saved_pat == pat and saved.group() == raw: m = saved if m is None: return False text = m.expand(template) c.insertText(text) return True def go_to_anchor(self, anchor): if anchor is TOP: c = self.textCursor() c.movePosition(c.Start) self.setTextCursor(c) return True base = r'''%%s\s*=\s*['"]{0,1}%s''' % regex.escape(anchor) raw = unicode(self.toPlainText()) m = regex.search(base % 'id', raw) if m is None: m = regex.search(base % 'name', raw) if m is not None: c = self.textCursor() c.setPosition(m.start()) self.setTextCursor(c) return True return False # }}} # Line numbers and cursor line {{{ def highlight_cursor_line(self): sel = QTextEdit.ExtraSelection() sel.format.setBackground(self.palette().alternateBase()) sel.format.setProperty(QTextFormat.FullWidthSelection, True) sel.cursor = self.textCursor() sel.cursor.clearSelection() self.current_cursor_line = sel self.update_extra_selections(instant=False) # Update the cursor line's line number in the line number area try: self.line_number_area.update(0, self.last_current_lnum[0], self.line_number_area.width(), self.last_current_lnum[1]) except AttributeError: pass block = self.textCursor().block() top = int(self.blockBoundingGeometry(block).translated(self.contentOffset()).top()) height = int(self.blockBoundingRect(block).height()) self.line_number_area.update(0, top, self.line_number_area.width(), height) def update_line_number_area_width(self, block_count=0): self.gutter_width = self.line_number_area_width() self.setViewportMargins(self.gutter_width, 0, 0, 0) def line_number_area_width(self): digits = 1 limit = max(1, self.blockCount()) while limit >= 10: limit /= 10 digits += 1 return 8 + self.number_width * digits def update_line_number_area(self, rect, dy): if dy: self.line_number_area.scroll(0, dy) else: self.line_number_area.update(0, rect.y(), self.line_number_area.width(), rect.height()) if rect.contains(self.viewport().rect()): self.update_line_number_area_width() def resizeEvent(self, ev): QPlainTextEdit.resizeEvent(self, ev) cr = self.contentsRect() self.line_number_area.setGeometry(QRect(cr.left(), cr.top(), self.line_number_area_width(), cr.height())) def paint_line_numbers(self, ev): painter = QPainter(self.line_number_area) painter.fillRect(ev.rect(), self.line_number_palette.color(QPalette.Base)) block = self.firstVisibleBlock() num = block.blockNumber() top = int(self.blockBoundingGeometry(block).translated(self.contentOffset()).top()) bottom = top + int(self.blockBoundingRect(block).height()) current = self.textCursor().block().blockNumber() painter.setPen(self.line_number_palette.color(QPalette.Text)) while block.isValid() and top <= ev.rect().bottom(): if block.isVisible() and bottom >= ev.rect().top(): if current == num: painter.save() painter.setPen(self.line_number_palette.color(QPalette.BrightText)) f = QFont(self.font()) f.setBold(True) painter.setFont(f) self.last_current_lnum = (top, bottom - top) painter.drawText(0, top, self.line_number_area.width() - 5, self.fontMetrics().height(), Qt.AlignRight, str(num + 1)) if current == num: painter.restore() block = block.next() top = bottom bottom = top + int(self.blockBoundingRect(block).height()) num += 1 # }}} def event(self, ev): if ev.type() == ev.ToolTip: self.show_tooltip(ev) return True if ev.type() == ev.ShortcutOverride: if ev in ( # Let the global cut/copy/paste/undo/redo shortcuts work,this avoids the nbsp # problem as well, since they use the overridden copy() method # instead of the one from Qt, and allows proper customization # of the shortcuts QKeySequence.Copy, QKeySequence.Cut, QKeySequence.Paste, QKeySequence.Undo, QKeySequence.Redo ) or ( # This is used to convert typed hex codes into unicode # characters ev.key() == Qt.Key_X and ev.modifiers() == Qt.AltModifier ): ev.ignore() return False return QPlainTextEdit.event(self, ev) def text_for_range(self, block, r): c = self.textCursor() c.setPosition(block.position() + r.start) c.setPosition(c.position() + r.length, c.KeepAnchor) return unicode(c.selectedText()) def spellcheck_locale_for_cursor(self, c): with store_locale: formats = self.highlighter.parse_single_block(c.block())[0] pos = c.positionInBlock() for r in formats: if r.start <= pos < r.start + r.length and r.format.property(SPELL_PROPERTY): return r.format.property(SPELL_LOCALE_PROPERTY) def recheck_word(self, word, locale): c = self.textCursor() c.movePosition(c.Start) block = c.block() while block.isValid(): for r in block.layout().additionalFormats(): if r.format.property(SPELL_PROPERTY) and self.text_for_range(block, r) == word: self.highlighter.reformat_block(block) break block = block.next() # Tooltips {{{ def syntax_range_for_cursor(self, cursor): if cursor.isNull(): return pos = cursor.positionInBlock() for r in cursor.block().layout().additionalFormats(): if r.start <= pos < r.start + r.length and r.format.property(SYNTAX_PROPERTY): return r def syntax_format_for_cursor(self, cursor): return getattr(self.syntax_range_for_cursor(cursor), 'format', None) def show_tooltip(self, ev): c = self.cursorForPosition(ev.pos()) fmt = self.syntax_format_for_cursor(c) if fmt is not None: tt = unicode(fmt.toolTip()) if tt: QToolTip.setFont(self.tooltip_font) QToolTip.setPalette(self.tooltip_palette) QToolTip.showText(ev.globalPos(), textwrap.fill(tt)) return QToolTip.hideText() ev.ignore() # }}} def link_for_position(self, pos): c = self.cursorForPosition(pos) r = self.syntax_range_for_cursor(c) if r is not None and r.format.property(LINK_PROPERTY): return self.text_for_range(c.block(), r) def mousePressEvent(self, ev): if ev.modifiers() & Qt.CTRL: url = self.link_for_position(ev.pos()) if url is not None: ev.accept() self.link_clicked.emit(url) return return PlainTextEdit.mousePressEvent(self, ev) def get_range_inside_tag(self): c = self.textCursor() left = min(c.anchor(), c.position()) right = max(c.anchor(), c.position()) # For speed we use QPlainTextEdit's toPlainText as we dont care about # spaces in this context raw = unicode(QPlainTextEdit.toPlainText(self)) # Make sure the left edge is not within a <> gtpos = raw.find('>', left) ltpos = raw.find('<', left) if gtpos < ltpos: left = gtpos + 1 if gtpos > -1 else left right = max(left, right) if right != left: gtpos = raw.find('>', right) ltpos = raw.find('<', right) if ltpos > gtpos: ltpos = raw.rfind('<', left, right+1) right = max(ltpos, left) return left, right def format_text(self, formatting): if self.syntax != 'html': return if formatting.startswith('justify_'): return self.smarts.set_text_alignment(self, formatting.partition('_')[-1]) color = 'currentColor' if formatting in {'color', 'background-color'}: color = QColorDialog.getColor(QColor(Qt.black if formatting == 'color' else Qt.white), self, _('Choose color'), QColorDialog.ShowAlphaChannel) if not color.isValid(): return r, g, b, a = color.getRgb() if a == 255: color = 'rgb(%d, %d, %d)' % (r, g, b) else: color = 'rgba(%d, %d, %d, %.2g)' % (r, g, b, a/255) prefix, suffix = { 'bold': ('<b>', '</b>'), 'italic': ('<i>', '</i>'), 'underline': ('<u>', '</u>'), 'strikethrough': ('<strike>', '</strike>'), 'superscript': ('<sup>', '</sup>'), 'subscript': ('<sub>', '</sub>'), 'color': ('<span style="color: %s">' % color, '</span>'), 'background-color': ('<span style="background-color: %s">' % color, '</span>'), }[formatting] left, right = self.get_range_inside_tag() c = self.textCursor() c.setPosition(left) c.setPosition(right, c.KeepAnchor) prev_text = unicode(c.selectedText()).rstrip('\0') c.insertText(prefix + prev_text + suffix) if prev_text: right = c.position() c.setPosition(left) c.setPosition(right, c.KeepAnchor) else: c.setPosition(c.position() - len(suffix)) self.setTextCursor(c) def insert_image(self, href): c = self.textCursor() template, alt = 'url(%s)', '' left = min(c.position(), c.anchor) if self.syntax == 'html': left, right = self.get_range_inside_tag() c.setPosition(left) c.setPosition(right, c.KeepAnchor) alt = _('Image') template = '<img alt="{0}" src="%s" />'.format(alt) href = prepare_string_for_xml(href, True) text = template % href c.insertText(text) if self.syntax == 'html': c.setPosition(left + 10) c.setPosition(c.position() + len(alt), c.KeepAnchor) else: c.setPosition(left) c.setPosition(left + len(text), c.KeepAnchor) self.setTextCursor(c) def insert_hyperlink(self, target, text): if hasattr(self.smarts, 'insert_hyperlink'): self.smarts.insert_hyperlink(self, target, text) def insert_tag(self, tag): if hasattr(self.smarts, 'insert_tag'): self.smarts.insert_tag(self, tag) def keyPressEvent(self, ev): if ev.key() == Qt.Key_X and ev.modifiers() == Qt.AltModifier: if self.replace_possible_unicode_sequence(): ev.accept() return if ev.key() == Qt.Key_Insert: self.setOverwriteMode(self.overwriteMode() ^ True) ev.accept() return if isosx and ev.modifiers() == Qt.ControlModifier and re.search(r'[a-zA-Z0-9]+', ev.text()) is not None: # For some reason Qt 5 translates Cmd+key into text on OS X # https://bugreports.qt-project.org/browse/QTBUG-36281 ev.setAccepted(False) return QPlainTextEdit.keyPressEvent(self, ev) if (ev.key() == Qt.Key_Semicolon or ';' in unicode(ev.text())) and tprefs['replace_entities_as_typed'] and self.syntax == 'html': self.replace_possible_entity() def replace_possible_unicode_sequence(self): c = self.textCursor() has_selection = c.hasSelection() if has_selection: text = unicode(c.selectedText()).rstrip('\0') else: c.setPosition(c.position() - min(c.positionInBlock(), 6), c.KeepAnchor) text = unicode(c.selectedText()).rstrip('\0') m = re.search(r'[a-fA-F0-9]{2,6}$', text) if m is None: return False text = m.group() try: num = int(text, 16) except ValueError: return False if num > 0x10ffff or num < 1: return False end_pos = max(c.anchor(), c.position()) c.setPosition(end_pos - len(text)), c.setPosition(end_pos, c.KeepAnchor) c.insertText(safe_chr(num)) return True def replace_possible_entity(self): c = self.textCursor() c.setPosition(c.position() - min(c.positionInBlock(), 10), c.KeepAnchor) text = unicode(c.selectedText()).rstrip('\0') m = entity_pat.search(text) if m is None: return ent = m.group() repl = xml_entity_to_unicode(m) if repl != ent: c.setPosition(c.position() + m.start(), c.KeepAnchor) c.insertText(repl) def select_all(self): c = self.textCursor() c.clearSelection() c.setPosition(0) c.movePosition(c.End, c.KeepAnchor) self.setTextCursor(c) def rename_block_tag(self, new_name): if hasattr(self.smarts, 'rename_block_tag'): self.smarts.rename_block_tag(self, new_name) def current_tag(self, for_position_sync=True): return self.smarts.cursor_position_with_sourceline(self.textCursor(), for_position_sync=for_position_sync) def goto_sourceline(self, sourceline, tags, attribute=None): return self.smarts.goto_sourceline(self, sourceline, tags, attribute=attribute) def get_tag_contents(self): c = self.smarts.get_inner_HTML(self) if c is not None: return self.selected_text_from_cursor(c) def goto_css_rule(self, rule_address, sourceline_address=None): from calibre.gui2.tweak_book.editor.smart.css import find_rule block = None if self.syntax == 'css': raw = unicode(self.toPlainText()) line, col = find_rule(raw, rule_address) if line is not None: block = self.document().findBlockByNumber(line - 1) elif sourceline_address is not None: sourceline, tags = sourceline_address if self.goto_sourceline(sourceline, tags): c = self.textCursor() c.setPosition(c.position() + 1) self.setTextCursor(c) raw = self.get_tag_contents() line, col = find_rule(raw, rule_address) if line is not None: block = self.document().findBlockByNumber(c.blockNumber() + line - 1) if block is not None and block.isValid(): c = self.textCursor() c.setPosition(block.position() + col) self.setTextCursor(c) def change_case(self, action, cursor=None): cursor = cursor or self.textCursor() text = self.selected_text_from_cursor(cursor) text = {'lower':lower, 'upper':upper, 'capitalize':capitalize, 'title':titlecase, 'swap':swapcase}[action](text) cursor.insertText(text) self.setTextCursor(cursor)
def paintEvent(self, event): contents_y = self.edit.verticalScrollBar().value() page_bottom = contents_y + self.edit.viewport().height() font_metrics = self.fontMetrics() current_block = self.edit.document().findBlock( self.edit.textCursor().position()) painter = QPainter(self) first_line = self.edit.firstVisibleBlock().firstLineNumber() # print(self.highest_line.data()) line_count = 0 # Iterate over all text blocks in the document. block = self.edit.document().begin() while block.isValid(): line_count += 1 # The top left position of the block in the document position = self.edit.document().documentLayout().blockBoundingRect( block).topLeft() # Check if the position of the block is out side of the visible # area. if position.y() > page_bottom: break # We want the line number for the selected line to be bold. bold = False if block == current_block: bold = True font = painter.font() font.setBold(True) painter.setFont(font) # Draw the line number right justified at the y position of the # line. 3 is a magic padding number. drawText(x, y, text). font = QFont() font.setPointSize(self.edit.font().pointSize()) font.setWordSpacing(20) painter.setFont(font) if line_count == 1: painter.drawText( self.width() - font_metrics.width(str(line_count + first_line)) - 3, 1 + 17 * line_count, str(first_line + line_count)) else: painter.drawText( self.width() - font_metrics.width(str(line_count + first_line)) - 3, 1 + 17 * line_count + (self.edit.font().pointSize() - 10) * line_count * 2, str(first_line + line_count)) #print(self.width() - font_metrics.width(str(line_count)) - 3 , " " , round(position.y()) - contents_y + font_metrics.ascent(), str(line_count)) # Remove the bold style if it was set previously. if bold: font = painter.font() font.setBold(False) painter.setFont(font) block = block.next() self.highest_line = line_count painter.end() QWidget.paintEvent(self, event)
class TextEdit(PlainTextEdit): link_clicked = pyqtSignal(object) smart_highlighting_updated = pyqtSignal() def __init__(self, parent=None, expected_geometry=(100, 50)): PlainTextEdit.__init__(self, parent) self.gutter_width = 0 self.tw = 2 self.expected_geometry = expected_geometry self.saved_matches = {} self.syntax = None self.smarts = NullSmarts(self) self.current_cursor_line = None self.current_search_mark = None self.smarts_highlight_timer = t = QTimer() t.setInterval(750), t.setSingleShot(True), t.timeout.connect(self.update_extra_selections) self.highlighter = SyntaxHighlighter() self.line_number_area = LineNumbers(self) self.apply_settings() self.setMouseTracking(True) self.cursorPositionChanged.connect(self.highlight_cursor_line) self.blockCountChanged[int].connect(self.update_line_number_area_width) self.updateRequest.connect(self.update_line_number_area) @dynamic_property def is_modified(self): ''' True if the document has been modified since it was loaded or since the last time is_modified was set to False. ''' def fget(self): return self.document().isModified() def fset(self, val): self.document().setModified(bool(val)) return property(fget=fget, fset=fset) def sizeHint(self): return self.size_hint def apply_settings(self, prefs=None, dictionaries_changed=False): # {{{ prefs = prefs or tprefs self.setLineWrapMode(QPlainTextEdit.WidgetWidth if prefs['editor_line_wrap'] else QPlainTextEdit.NoWrap) theme = get_theme(prefs['editor_theme']) self.apply_theme(theme) w = self.fontMetrics() self.space_width = w.width(' ') self.tw = self.smarts.override_tab_stop_width if self.smarts.override_tab_stop_width is not None else prefs['editor_tab_stop_width'] self.setTabStopWidth(self.tw * self.space_width) if dictionaries_changed: self.highlighter.rehighlight() def apply_theme(self, theme): self.theme = theme pal = self.palette() pal.setColor(pal.Base, theme_color(theme, 'Normal', 'bg')) pal.setColor(pal.AlternateBase, theme_color(theme, 'CursorLine', 'bg')) pal.setColor(pal.Text, theme_color(theme, 'Normal', 'fg')) pal.setColor(pal.Highlight, theme_color(theme, 'Visual', 'bg')) pal.setColor(pal.HighlightedText, theme_color(theme, 'Visual', 'fg')) self.setPalette(pal) self.tooltip_palette = pal = QPalette() pal.setColor(pal.ToolTipBase, theme_color(theme, 'Tooltip', 'bg')) pal.setColor(pal.ToolTipText, theme_color(theme, 'Tooltip', 'fg')) self.line_number_palette = pal = QPalette() pal.setColor(pal.Base, theme_color(theme, 'LineNr', 'bg')) pal.setColor(pal.Text, theme_color(theme, 'LineNr', 'fg')) pal.setColor(pal.BrightText, theme_color(theme, 'LineNrC', 'fg')) self.match_paren_format = theme_format(theme, 'MatchParen') font = self.font() ff = tprefs['editor_font_family'] if ff is None: ff = default_font_family() font.setFamily(ff) font.setPointSize(tprefs['editor_font_size']) self.tooltip_font = QFont(font) self.tooltip_font.setPointSize(font.pointSize() - 1) self.setFont(font) self.highlighter.apply_theme(theme) w = self.fontMetrics() self.number_width = max(map(lambda x:w.width(str(x)), xrange(10))) self.size_hint = QSize(self.expected_geometry[0] * w.averageCharWidth(), self.expected_geometry[1] * w.height()) self.highlight_color = theme_color(theme, 'HighlightRegion', 'bg') self.highlight_cursor_line() # }}} def load_text(self, text, syntax='html', process_template=False, doc_name=None): self.syntax = syntax self.highlighter = get_highlighter(syntax)() self.highlighter.apply_theme(self.theme) self.highlighter.set_document(self.document(), doc_name=doc_name) sclass = get_smarts(syntax) if sclass is not None: self.smarts = sclass(self) if self.smarts.override_tab_stop_width is not None: self.tw = self.smarts.override_tab_stop_width self.setTabStopWidth(self.tw * self.space_width) self.setPlainText(unicodedata.normalize('NFC', unicode(text))) if process_template and QPlainTextEdit.find(self, '%CURSOR%'): c = self.textCursor() c.insertText('') def change_document_name(self, newname): self.highlighter.doc_name = newname self.highlighter.rehighlight() # Ensure links are checked w.r.t. the new name correctly def replace_text(self, text): c = self.textCursor() pos = c.position() c.beginEditBlock() c.clearSelection() c.select(c.Document) c.insertText(unicodedata.normalize('NFC', text)) c.endEditBlock() c.setPosition(min(pos, len(text))) self.setTextCursor(c) self.ensureCursorVisible() def simple_replace(self, text, cursor=None): c = cursor or self.textCursor() c.insertText(unicodedata.normalize('NFC', text)) self.setTextCursor(c) def go_to_line(self, lnum, col=None): lnum = max(1, min(self.blockCount(), lnum)) c = self.textCursor() c.clearSelection() c.movePosition(c.Start) c.movePosition(c.NextBlock, n=lnum - 1) c.movePosition(c.StartOfLine) c.movePosition(c.EndOfLine, c.KeepAnchor) text = unicode(c.selectedText()).rstrip('\0') if col is None: c.movePosition(c.StartOfLine) lt = text.lstrip() if text and lt and lt != text: c.movePosition(c.NextWord) else: c.setPosition(c.block().position() + col) if c.blockNumber() + 1 > lnum: # We have moved past the end of the line c.setPosition(c.block().position()) c.movePosition(c.EndOfBlock) self.setTextCursor(c) self.ensureCursorVisible() def update_extra_selections(self, instant=True): sel = [] if self.current_cursor_line is not None: sel.append(self.current_cursor_line) if self.current_search_mark is not None: sel.append(self.current_search_mark) if instant and not self.highlighter.has_requests and self.smarts is not None: sel.extend(self.smarts.get_extra_selections(self)) self.smart_highlighting_updated.emit() else: self.smarts_highlight_timer.start() self.setExtraSelections(sel) # Search and replace {{{ def mark_selected_text(self): sel = QTextEdit.ExtraSelection() sel.format.setBackground(self.highlight_color) sel.cursor = self.textCursor() if sel.cursor.hasSelection(): self.current_search_mark = sel c = self.textCursor() c.clearSelection() self.setTextCursor(c) else: self.current_search_mark = None self.update_extra_selections() def find_in_marked(self, pat, wrap=False, save_match=None): if self.current_search_mark is None: return False csm = self.current_search_mark.cursor reverse = pat.flags & regex.REVERSE c = self.textCursor() c.clearSelection() m_start = min(csm.position(), csm.anchor()) m_end = max(csm.position(), csm.anchor()) if c.position() < m_start: c.setPosition(m_start) if c.position() > m_end: c.setPosition(m_end) pos = m_start if reverse else m_end if wrap: pos = m_end if reverse else m_start c.setPosition(pos, c.KeepAnchor) raw = unicode(c.selectedText()).replace(PARAGRAPH_SEPARATOR, '\n').rstrip('\0') m = pat.search(raw) if m is None: return False start, end = m.span() if start == end: return False if wrap: if reverse: textpos = c.anchor() start, end = textpos + end, textpos + start else: start, end = m_start + start, m_start + end else: if reverse: start, end = m_start + end, m_start + start else: start, end = c.anchor() + start, c.anchor() + end c.clearSelection() c.setPosition(start) c.setPosition(end, c.KeepAnchor) self.setTextCursor(c) # Center search result on screen self.centerCursor() if save_match is not None: self.saved_matches[save_match] = (pat, m) return True def all_in_marked(self, pat, template=None): if self.current_search_mark is None: return 0 c = self.current_search_mark.cursor raw = unicode(c.selectedText()).replace(PARAGRAPH_SEPARATOR, '\n').rstrip('\0') if template is None: count = len(pat.findall(raw)) else: from calibre.gui2.tweak_book.function_replace import Function repl_is_func = isinstance(template, Function) if repl_is_func: template.init_env() raw, count = pat.subn(template, raw) if repl_is_func: from calibre.gui2.tweak_book.search import show_function_debug_output template.end() show_function_debug_output(template) if count > 0: start_pos = min(c.anchor(), c.position()) c.insertText(raw) end_pos = max(c.anchor(), c.position()) c.setPosition(start_pos), c.setPosition(end_pos, c.KeepAnchor) self.update_extra_selections() return count def find(self, pat, wrap=False, marked=False, complete=False, save_match=None): if marked: return self.find_in_marked(pat, wrap=wrap, save_match=save_match) reverse = pat.flags & regex.REVERSE c = self.textCursor() c.clearSelection() if complete: # Search the entire text c.movePosition(c.End if reverse else c.Start) pos = c.Start if reverse else c.End if wrap and not complete: pos = c.End if reverse else c.Start c.movePosition(pos, c.KeepAnchor) raw = unicode(c.selectedText()).replace(PARAGRAPH_SEPARATOR, '\n').rstrip('\0') m = pat.search(raw) if m is None: return False start, end = m.span() if start == end: return False if wrap and not complete: if reverse: textpos = c.anchor() start, end = textpos + end, textpos + start else: if reverse: # Put the cursor at the start of the match start, end = end, start else: textpos = c.anchor() start, end = textpos + start, textpos + end c.clearSelection() c.setPosition(start) c.setPosition(end, c.KeepAnchor) self.setTextCursor(c) # Center search result on screen self.centerCursor() if save_match is not None: self.saved_matches[save_match] = (pat, m) return True def find_spell_word(self, original_words, lang, from_cursor=True, center_on_cursor=True): c = self.textCursor() c.setPosition(c.position()) if not from_cursor: c.movePosition(c.Start) c.movePosition(c.End, c.KeepAnchor) def find_first_word(haystack): match_pos, match_word = -1, None for w in original_words: idx = index_of(w, haystack, lang=lang) if idx > -1 and (match_pos == -1 or match_pos > idx): match_pos, match_word = idx, w return match_pos, match_word while True: text = unicode(c.selectedText()).rstrip('\0') idx, word = find_first_word(text) if idx == -1: return False c.setPosition(c.anchor() + idx) c.setPosition(c.position() + string_length(word), c.KeepAnchor) if self.smarts.verify_for_spellcheck(c, self.highlighter): self.setTextCursor(c) if center_on_cursor: self.centerCursor() return True c.setPosition(c.position()) c.movePosition(c.End, c.KeepAnchor) return False def find_next_spell_error(self, from_cursor=True): c = self.textCursor() if not from_cursor: c.movePosition(c.Start) block = c.block() while block.isValid(): for r in block.layout().additionalFormats(): if r.format.property(SPELL_PROPERTY): if not from_cursor or block.position() + r.start + r.length > c.position(): c.setPosition(block.position() + r.start) c.setPosition(c.position() + r.length, c.KeepAnchor) self.setTextCursor(c) return True block = block.next() return False def replace(self, pat, template, saved_match='gui'): c = self.textCursor() raw = unicode(c.selectedText()).replace(PARAGRAPH_SEPARATOR, '\n').rstrip('\0') m = pat.fullmatch(raw) if m is None: # This can happen if either the user changed the selected text or # the search expression uses lookahead/lookbehind operators. See if # the saved match matches the currently selected text and # use it, if so. if saved_match is not None and saved_match in self.saved_matches: saved_pat, saved = self.saved_matches.pop(saved_match) if saved_pat == pat and saved.group() == raw: m = saved if m is None: return False if callable(template): text = template(m) else: text = m.expand(template) c.insertText(text) return True def go_to_anchor(self, anchor): if anchor is TOP: c = self.textCursor() c.movePosition(c.Start) self.setTextCursor(c) return True base = r'''%%s\s*=\s*['"]{0,1}%s''' % regex.escape(anchor) raw = unicode(self.toPlainText()) m = regex.search(base % 'id', raw) if m is None: m = regex.search(base % 'name', raw) if m is not None: c = self.textCursor() c.setPosition(m.start()) self.setTextCursor(c) return True return False # }}} # Line numbers and cursor line {{{ def highlight_cursor_line(self): sel = QTextEdit.ExtraSelection() sel.format.setBackground(self.palette().alternateBase()) sel.format.setProperty(QTextFormat.FullWidthSelection, True) sel.cursor = self.textCursor() sel.cursor.clearSelection() self.current_cursor_line = sel self.update_extra_selections(instant=False) # Update the cursor line's line number in the line number area try: self.line_number_area.update(0, self.last_current_lnum[0], self.line_number_area.width(), self.last_current_lnum[1]) except AttributeError: pass block = self.textCursor().block() top = int(self.blockBoundingGeometry(block).translated(self.contentOffset()).top()) height = int(self.blockBoundingRect(block).height()) self.line_number_area.update(0, top, self.line_number_area.width(), height) def update_line_number_area_width(self, block_count=0): self.gutter_width = self.line_number_area_width() self.setViewportMargins(self.gutter_width, 0, 0, 0) def line_number_area_width(self): digits = 1 limit = max(1, self.blockCount()) while limit >= 10: limit /= 10 digits += 1 return 8 + self.number_width * digits def update_line_number_area(self, rect, dy): if dy: self.line_number_area.scroll(0, dy) else: self.line_number_area.update(0, rect.y(), self.line_number_area.width(), rect.height()) if rect.contains(self.viewport().rect()): self.update_line_number_area_width() def resizeEvent(self, ev): QPlainTextEdit.resizeEvent(self, ev) cr = self.contentsRect() self.line_number_area.setGeometry(QRect(cr.left(), cr.top(), self.line_number_area_width(), cr.height())) def paint_line_numbers(self, ev): painter = QPainter(self.line_number_area) painter.fillRect(ev.rect(), self.line_number_palette.color(QPalette.Base)) block = self.firstVisibleBlock() num = block.blockNumber() top = int(self.blockBoundingGeometry(block).translated(self.contentOffset()).top()) bottom = top + int(self.blockBoundingRect(block).height()) current = self.textCursor().block().blockNumber() painter.setPen(self.line_number_palette.color(QPalette.Text)) while block.isValid() and top <= ev.rect().bottom(): if block.isVisible() and bottom >= ev.rect().top(): if current == num: painter.save() painter.setPen(self.line_number_palette.color(QPalette.BrightText)) f = QFont(self.font()) f.setBold(True) painter.setFont(f) self.last_current_lnum = (top, bottom - top) painter.drawText(0, top, self.line_number_area.width() - 5, self.fontMetrics().height(), Qt.AlignRight, str(num + 1)) if current == num: painter.restore() block = block.next() top = bottom bottom = top + int(self.blockBoundingRect(block).height()) num += 1 # }}} def event(self, ev): if ev.type() == ev.ToolTip: self.show_tooltip(ev) return True if ev.type() == ev.ShortcutOverride: if ev in ( # Let the global cut/copy/paste/undo/redo shortcuts work,this avoids the nbsp # problem as well, since they use the overridden copy() method # instead of the one from Qt, and allows proper customization # of the shortcuts QKeySequence.Copy, QKeySequence.Cut, QKeySequence.Paste, QKeySequence.Undo, QKeySequence.Redo ) or ( # This is used to convert typed hex codes into unicode # characters ev.key() == Qt.Key_X and ev.modifiers() == Qt.AltModifier ): ev.ignore() return False return QPlainTextEdit.event(self, ev) def text_for_range(self, block, r): c = self.textCursor() c.setPosition(block.position() + r.start) c.setPosition(c.position() + r.length, c.KeepAnchor) return unicode(c.selectedText()) def spellcheck_locale_for_cursor(self, c): with store_locale: formats = self.highlighter.parse_single_block(c.block())[0] pos = c.positionInBlock() for r in formats: if r.start <= pos < r.start + r.length and r.format.property(SPELL_PROPERTY): return r.format.property(SPELL_LOCALE_PROPERTY) def recheck_word(self, word, locale): c = self.textCursor() c.movePosition(c.Start) block = c.block() while block.isValid(): for r in block.layout().additionalFormats(): if r.format.property(SPELL_PROPERTY) and self.text_for_range(block, r) == word: self.highlighter.reformat_block(block) break block = block.next() # Tooltips {{{ def syntax_range_for_cursor(self, cursor): if cursor.isNull(): return pos = cursor.positionInBlock() for r in cursor.block().layout().additionalFormats(): if r.start <= pos < r.start + r.length and r.format.property(SYNTAX_PROPERTY): return r def syntax_format_for_cursor(self, cursor): return getattr(self.syntax_range_for_cursor(cursor), 'format', None) def show_tooltip(self, ev): c = self.cursorForPosition(ev.pos()) fmt = self.syntax_format_for_cursor(c) if fmt is not None: tt = unicode(fmt.toolTip()) if tt: QToolTip.setFont(self.tooltip_font) QToolTip.setPalette(self.tooltip_palette) QToolTip.showText(ev.globalPos(), textwrap.fill(tt)) return QToolTip.hideText() ev.ignore() # }}} def link_for_position(self, pos): c = self.cursorForPosition(pos) r = self.syntax_range_for_cursor(c) if r is not None and r.format.property(LINK_PROPERTY): return self.text_for_range(c.block(), r) def mousePressEvent(self, ev): if ev.modifiers() & Qt.CTRL: url = self.link_for_position(ev.pos()) if url is not None: ev.accept() self.link_clicked.emit(url) return return PlainTextEdit.mousePressEvent(self, ev) def get_range_inside_tag(self): c = self.textCursor() left = min(c.anchor(), c.position()) right = max(c.anchor(), c.position()) # For speed we use QPlainTextEdit's toPlainText as we dont care about # spaces in this context raw = unicode(QPlainTextEdit.toPlainText(self)) # Make sure the left edge is not within a <> gtpos = raw.find('>', left) ltpos = raw.find('<', left) if gtpos < ltpos: left = gtpos + 1 if gtpos > -1 else left right = max(left, right) if right != left: gtpos = raw.find('>', right) ltpos = raw.find('<', right) if ltpos > gtpos: ltpos = raw.rfind('<', left, right+1) right = max(ltpos, left) return left, right def format_text(self, formatting): if self.syntax != 'html': return if formatting.startswith('justify_'): return self.smarts.set_text_alignment(self, formatting.partition('_')[-1]) color = 'currentColor' if formatting in {'color', 'background-color'}: color = QColorDialog.getColor(QColor(Qt.black if formatting == 'color' else Qt.white), self, _('Choose color'), QColorDialog.ShowAlphaChannel) if not color.isValid(): return r, g, b, a = color.getRgb() if a == 255: color = 'rgb(%d, %d, %d)' % (r, g, b) else: color = 'rgba(%d, %d, %d, %.2g)' % (r, g, b, a/255) prefix, suffix = { 'bold': ('<b>', '</b>'), 'italic': ('<i>', '</i>'), 'underline': ('<u>', '</u>'), 'strikethrough': ('<strike>', '</strike>'), 'superscript': ('<sup>', '</sup>'), 'subscript': ('<sub>', '</sub>'), 'color': ('<span style="color: %s">' % color, '</span>'), 'background-color': ('<span style="background-color: %s">' % color, '</span>'), }[formatting] left, right = self.get_range_inside_tag() c = self.textCursor() c.setPosition(left) c.setPosition(right, c.KeepAnchor) prev_text = unicode(c.selectedText()).rstrip('\0') c.insertText(prefix + prev_text + suffix) if prev_text: right = c.position() c.setPosition(left) c.setPosition(right, c.KeepAnchor) else: c.setPosition(c.position() - len(suffix)) self.setTextCursor(c) def insert_image(self, href): c = self.textCursor() template, alt = 'url(%s)', '' left = min(c.position(), c.anchor) if self.syntax == 'html': left, right = self.get_range_inside_tag() c.setPosition(left) c.setPosition(right, c.KeepAnchor) alt = _('Image') template = '<img alt="{0}" src="%s" />'.format(alt) href = prepare_string_for_xml(href, True) text = template % href c.insertText(text) if self.syntax == 'html': c.setPosition(left + 10) c.setPosition(c.position() + len(alt), c.KeepAnchor) else: c.setPosition(left) c.setPosition(left + len(text), c.KeepAnchor) self.setTextCursor(c) def insert_hyperlink(self, target, text): if hasattr(self.smarts, 'insert_hyperlink'): self.smarts.insert_hyperlink(self, target, text) def insert_tag(self, tag): if hasattr(self.smarts, 'insert_tag'): self.smarts.insert_tag(self, tag) def keyPressEvent(self, ev): if ev.key() == Qt.Key_X and ev.modifiers() == Qt.AltModifier: if self.replace_possible_unicode_sequence(): ev.accept() return if ev.key() == Qt.Key_Insert: self.setOverwriteMode(self.overwriteMode() ^ True) ev.accept() return if isosx and QT_VERSION < 0x504000 and ev.modifiers() == Qt.ControlModifier and re.search(r'[a-zA-Z0-9]+', ev.text()) is not None: # For some reason Qt 5 translates Cmd+key into text on OS X # https://bugreports.qt-project.org/browse/QTBUG-36281 ev.setAccepted(False) return if self.smarts.handle_key_press(ev, self): return QPlainTextEdit.keyPressEvent(self, ev) def replace_possible_unicode_sequence(self): c = self.textCursor() has_selection = c.hasSelection() if has_selection: text = unicode(c.selectedText()).rstrip('\0') else: c.setPosition(c.position() - min(c.positionInBlock(), 6), c.KeepAnchor) text = unicode(c.selectedText()).rstrip('\0') m = re.search(r'[a-fA-F0-9]{2,6}$', text) if m is None: return False text = m.group() try: num = int(text, 16) except ValueError: return False if num > 0x10ffff or num < 1: return False end_pos = max(c.anchor(), c.position()) c.setPosition(end_pos - len(text)), c.setPosition(end_pos, c.KeepAnchor) c.insertText(safe_chr(num)) return True def select_all(self): c = self.textCursor() c.clearSelection() c.setPosition(0) c.movePosition(c.End, c.KeepAnchor) self.setTextCursor(c) def rename_block_tag(self, new_name): if hasattr(self.smarts, 'rename_block_tag'): self.smarts.rename_block_tag(self, new_name) def current_tag(self, for_position_sync=True): return self.smarts.cursor_position_with_sourceline(self.textCursor(), for_position_sync=for_position_sync) def goto_sourceline(self, sourceline, tags, attribute=None): return self.smarts.goto_sourceline(self, sourceline, tags, attribute=attribute) def get_tag_contents(self): c = self.smarts.get_inner_HTML(self) if c is not None: return self.selected_text_from_cursor(c) def goto_css_rule(self, rule_address, sourceline_address=None): from calibre.gui2.tweak_book.editor.smarts.css import find_rule block = None if self.syntax == 'css': raw = unicode(self.toPlainText()) line, col = find_rule(raw, rule_address) if line is not None: block = self.document().findBlockByNumber(line - 1) elif sourceline_address is not None: sourceline, tags = sourceline_address if self.goto_sourceline(sourceline, tags): c = self.textCursor() c.setPosition(c.position() + 1) self.setTextCursor(c) raw = self.get_tag_contents() line, col = find_rule(raw, rule_address) if line is not None: block = self.document().findBlockByNumber(c.blockNumber() + line - 1) if block is not None and block.isValid(): c = self.textCursor() c.setPosition(block.position() + col) self.setTextCursor(c) def change_case(self, action, cursor=None): cursor = cursor or self.textCursor() text = self.selected_text_from_cursor(cursor) text = {'lower':lower, 'upper':upper, 'capitalize':capitalize, 'title':titlecase, 'swap':swapcase}[action](text) cursor.insertText(text) self.setTextCursor(cursor)
class MyBlockingBusy(QDialog): # {{{ all_done = pyqtSignal() def __init__(self, args, ids, db, refresh_books, cc_widgets, s_r_func, do_sr, sr_calls, parent=None, window_title=_('Working')): QDialog.__init__(self, parent) self._layout = l = QVBoxLayout() self.setLayout(l) self.msg = QLabel(_('Processing %d books, please wait...') % len(ids)) self.font = QFont() self.font.setPointSize(self.font.pointSize() + 8) self.msg.setFont(self.font) self.pi = ProgressIndicator(self) self.pi.setDisplaySize(100) self._layout.addWidget(self.pi, 0, Qt.AlignHCenter) self._layout.addSpacing(15) self._layout.addWidget(self.msg, 0, Qt.AlignHCenter) self.setWindowTitle(window_title + '...') self.setMinimumWidth(200) self.resize(self.sizeHint()) self.error = None self.all_done.connect(self.on_all_done, type=Qt.QueuedConnection) self.args, self.ids = args, ids self.db, self.cc_widgets = db, cc_widgets self.s_r_func = FunctionDispatcher(s_r_func) self.do_sr = do_sr self.sr_calls = sr_calls self.refresh_books = refresh_books def accept(self): pass def reject(self): pass def on_all_done(self): if not self.error: # The cc widgets can only be accessed in the GUI thread try: for w in self.cc_widgets: w.commit(self.ids) except Exception as err: import traceback self.error = (err, traceback.format_exc()) self.pi.stopAnimation() QDialog.accept(self) def exec_(self): self.thread = Thread(target=self.do_it) self.thread.start() self.pi.startAnimation() return QDialog.exec_(self) def do_it(self): try: self.do_all() except Exception as err: import traceback try: err = unicode(err) except: err = repr(err) self.error = (err, traceback.format_exc()) self.all_done.emit() def do_all(self): cache = self.db.new_api args = self.args # Title and authors if args.do_swap_ta: title_map = cache.all_field_for('title', self.ids) authors_map = cache.all_field_for('authors', self.ids) def new_title(authors): ans = authors_to_string(authors) return titlecase(ans) if args.do_title_case else ans new_title_map = {bid:new_title(authors) for bid, authors in authors_map.iteritems()} new_authors_map = {bid:string_to_authors(title) for bid, title in title_map.iteritems()} cache.set_field('authors', new_authors_map) cache.set_field('title', new_title_map) if args.do_title_case and not args.do_swap_ta: title_map = cache.all_field_for('title', self.ids) cache.set_field('title', {bid:titlecase(title) for bid, title in title_map.iteritems()}) if args.do_title_sort: lang_map = cache.all_field_for('languages', self.ids) title_map = cache.all_field_for('title', self.ids) def get_sort(book_id): if args.languages: lang = args.languages[0] else: try: lang = lang_map[book_id][0] except (KeyError, IndexError, TypeError, AttributeError): lang = 'eng' return title_sort(title_map[book_id], lang=lang) cache.set_field('sort', {bid:get_sort(bid) for bid in self.ids}) if args.au: authors = string_to_authors(args.au) cache.set_field('authors', {bid:authors for bid in self.ids}) if args.do_auto_author: aus_map = cache.author_sort_strings_for_books(self.ids) cache.set_field('author_sort', {book_id:' & '.join(aus_map[book_id]) for book_id in aus_map}) if args.aus and args.do_aus: cache.set_field('author_sort', {bid:args.aus for bid in self.ids}) # Covers if args.cover_action == 'remove': cache.set_cover({bid:None for bid in self.ids}) elif args.cover_action == 'generate': from calibre.ebooks import calibre_cover from calibre.ebooks.metadata import fmt_sidx from calibre.gui2 import config for book_id in self.ids: mi = self.db.get_metadata(book_id, index_is_id=True) series_string = None if mi.series: series_string = _('Book %(sidx)s of %(series)s')%dict( sidx=fmt_sidx(mi.series_index, use_roman=config['use_roman_numerals_for_series_number']), series=mi.series) cdata = calibre_cover(mi.title, mi.format_field('authors')[-1], series_string=series_string) cache.set_cover({book_id:cdata}) elif args.cover_action == 'fromfmt': for book_id in self.ids: fmts = cache.formats(book_id, verify_formats=False) if fmts: covers = [] for fmt in fmts: fmtf = cache.format(book_id, fmt, as_file=True) if fmtf is None: continue cdata, area = get_cover_data(fmtf, fmt) if cdata: covers.append((cdata, area)) covers.sort(key=lambda x: x[1]) if covers: cache.set_cover({book_id:covers[-1][0]}) elif args.cover_action == 'trim': from calibre.utils.magick import Image for book_id in self.ids: cdata = cache.cover(book_id) if cdata: im = Image() im.load(cdata) im.trim(tweaks['cover_trim_fuzz_value']) cdata = im.export('jpg') cache.set_cover({book_id:cdata}) elif args.cover_action == 'clone': cdata = None for book_id in self.ids: cdata = cache.cover(book_id) if cdata: break if cdata: cache.set_cover({bid:cdata for bid in self.ids if bid != book_id}) # Formats if args.do_remove_format: cache.remove_formats({bid:(args.remove_format,) for bid in self.ids}) if args.restore_original: for book_id in self.ids: formats = cache.formats(book_id) originals = tuple(x.upper() for x in formats if x.upper().startswith('ORIGINAL_')) for ofmt in originals: cache.restore_original_format(book_id, ofmt) # Various fields if args.rating != -1: cache.set_field('rating', {bid:args.rating*2 for bid in self.ids}) if args.clear_pub: cache.set_field('publisher', {bid:'' for bid in self.ids}) if args.pub: cache.set_field('publisher', {bid:args.pub for bid in self.ids}) if args.clear_series: cache.set_field('series', {bid:'' for bid in self.ids}) if args.pubdate is not None: cache.set_field('pubdate', {bid:args.pubdate for bid in self.ids}) if args.adddate is not None: cache.set_field('timestamp', {bid:args.adddate for bid in self.ids}) if args.do_series: sval = args.series_start_value if args.do_series_restart else cache.get_next_series_num_for(args.series, current_indices=True) cache.set_field('series', {bid:args.series for bid in self.ids}) if not args.series: cache.set_field('series_index', {bid:1.0 for bid in self.ids}) else: def next_series_num(bid, i): if args.do_series_restart: return sval + i next_num = _get_next_series_num_for_list(sorted(sval.itervalues()), unwrap=False) sval[bid] = next_num return next_num smap = {bid:next_series_num(bid, i) for i, bid in enumerate(self.ids)} if args.do_autonumber: cache.set_field('series_index', smap) elif tweaks['series_index_auto_increment'] != 'no_change': cache.set_field('series_index', {bid:1.0 for bid in self.ids}) if args.comments is not null: cache.set_field('comments', {bid:args.comments for bid in self.ids}) if args.do_remove_conv: cache.delete_conversion_options(self.ids) if args.clear_languages: cache.set_field('languages', {bid:() for bid in self.ids}) elif args.languages: cache.set_field('languages', {bid:args.languages for bid in self.ids}) if args.remove_all: cache.set_field('tags', {bid:() for bid in self.ids}) if args.add or args.remove: self.db.bulk_modify_tags(self.ids, add=args.add, remove=args.remove) if self.do_sr: for book_id in self.ids: self.s_r_func(book_id) if self.sr_calls: for field, book_id_val_map in self.sr_calls.iteritems(): self.refresh_books.update(self.db.new_api.set_field(field, book_id_val_map))
def do_size_hint(self, option, index): text = index.data(Qt.DisplayRole) or '' font = QFont(option.font) font.setPointSize(QFontInfo(font).pointSize() * 1.5) m = QFontMetrics(font) return QSize(m.width(text), m.height())
def __init__(self, parent, old, new): super(QDialog, self).__init__(parent.gui) self.db = parent.gui.current_db self.gui = parent.gui self._log_location() layout = QVBoxLayout() self.setLayout(layout) header = QLabel(_("Move annotations or change destination?")) header.setAlignment(Qt.AlignCenter) header_font = QFont() header_font.setPointSize(16) header.setFont(header_font) layout.addWidget(header) change_group = QGroupBox("", self) layout.addWidget(change_group) self.gl = QGridLayout() change_group.setLayout(self.gl) horizontal_line = QFrame(self) horizontal_line.setGeometry(QRect(0, 0, 1, 3)) horizontal_line.setFrameShape(QFrame.HLine) horizontal_line.setFrameShadow(QFrame.Raised) self.gl.addWidget(horizontal_line, 1, 0, 1, 2) self.move_button = QPushButton(_("Move")) self.gl.addWidget(self.move_button, 3, 0, 1, 1) self.move_label = QLabel( _('<html><head/><body><p>• Move existing annotations from <span style=" font-weight:600;">{old}</span> to <span style=" font-weight:600;">{new}</span>.<br/>• Existing annotations will be removed from <span style=" font-weight:600;">{old}</span>.<br/>• Newly imported annotations will be added to <span style=" font-weight:600;">{new}</span>.</p></body></html>' )) self.move_label.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding) self.gl.addWidget(self.move_label, 3, 1) self.gl.addWidget(horizontal_line, 4, 1) self.change_button = QPushButton(_("Change")) self.gl.addWidget(self.change_button, 5, 0, 1, 1) self.change_label = QLabel( _('<html><head/><body><p>• Change annotations storage from <span style=" font-weight:600;">{old}</span> to <span style=" font-weight:600;">{new}</span>.<br/>• Existing annotations will remain in <span style=" font-weight:600;">{old}</span>.<br/>• Newly imported annotations will be added to <span style=" font-weight:600;">{new}</span>.</p></body></html>' )) self.change_label.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding) self.gl.addWidget(self.change_label, 5, 1, 1, 1) self.bb = QDialogButtonBox(QDialogButtonBox.Cancel) layout.addWidget(self.bb) # Hook the button events self.bb.clicked.connect(partial(self.button_clicked, 'cancel')) self.move_button.clicked.connect(partial(self.button_clicked, 'move')) self.change_button.clicked.connect( partial(self.button_clicked, 'change')) # Customize the dialog text self.move_label.setText( str(self.move_label.text()).format(old=old, new=new)) self.change_label.setText( str(self.change_label.text()).format(old=old, new=new)) self.command = 'cancel' self.do_resize()
def createproxyTestPanel(self): self.buttonCheckProxy = QPushButton( self.translate("proxyTestPanel", "Check..."), self) labelproxyhostName = QLabel( self.translate("proxyTestPanel", "Proxy Host Name: "), self) self.lineEditproxyHostName = QLineEdit(self.proxyhostName) labelPort = QLabel(self.translate("proxyTestPanel", "Port: "), self) self.spinBoxPort = QSpinBox(self) self.spinBoxPort.setRange(0, 65535) self.spinBoxPort.setValue(1080) hboxPort = QHBoxLayout() hboxPort.addWidget(labelPort) hboxPort.addWidget(self.spinBoxPort) hboxPort.addStretch() labelTargethostUrl = QLabel( self.translate("proxyTestPanel", "Target Host Url: "), self) self.lineEdittargethostUrl = QLineEdit(self.url, self) labelProxyProtocol = QLabel( self.translate("proxyTestPanel", "Proxy Protocol: "), self) radioButtonSocks5 = QRadioButton("Socks5", self) radioButtonSocks5.setChecked(True) radioButtonHttp = QRadioButton("Http", self) self.groupRadioButton = QButtonGroup() self.groupRadioButton.addButton(radioButtonSocks5) self.groupRadioButton.addButton(radioButtonHttp) hboxRadioButtonProxyProtocol = QHBoxLayout() hboxRadioButtonProxyProtocol.addWidget(radioButtonSocks5) hboxRadioButtonProxyProtocol.addWidget(radioButtonHttp) hboxRadioButtonProxyProtocol.addStretch() labelProxyStatus = QLabel( self.translate("proxyTestPanel", "Proxy Status: "), self) labelProxyTimeLag = QLabel( self.translate("proxyTestPanel", "Proxy Time Lag: "), self) self.labelproxyStatus = QLabel() self.labelproxyTimeLag = QLabel() labelFont = QFont() labelFont.setPointSize(16) labelFont.setBold(True) self.labelproxyStatus.setFont(labelFont) self.labelproxyTimeLag.setFont(labelFont) self.palettelabelProxyStatusOK = QPalette() self.palettelabelProxyStatusOK.setColor(QPalette.WindowText, QColor(34, 139, 34)) # ##ForestGreen self.palettelabelProxyStatusFalse = QPalette() self.palettelabelProxyStatusFalse.setColor(QPalette.WindowText, Qt.red) self.palettelabelProxyTimeLagForestGreen = QPalette() self.palettelabelProxyTimeLagForestGreen.setColor( QPalette.WindowText, QColor(34, 139, 34)) self.palettelabelProxyTimeLagDarkOrange = QPalette() self.palettelabelProxyTimeLagDarkOrange.setColor( QPalette.WindowText, QColor(255, 140, 0)) self.palettelabelProxyTimeLagRed = QPalette() self.palettelabelProxyTimeLagRed.setColor(QPalette.WindowText, Qt.red) self.textEditProxy = QTextEdit() self.textEditProxy.setReadOnly(True) gridBoxProxy = QGridLayout() gridBoxProxy.addWidget(labelproxyhostName, 0, 0) gridBoxProxy.addWidget(self.lineEditproxyHostName, 0, 1) gridBoxProxy.addLayout(hboxPort, 0, 2) gridBoxProxy.addWidget(labelTargethostUrl, 1, 0) gridBoxProxy.addWidget(self.lineEdittargethostUrl, 1, 1) gridBoxProxy.addWidget(labelProxyProtocol, 2, 0) gridBoxProxy.addLayout(hboxRadioButtonProxyProtocol, 2, 1) gridBoxProxy.addWidget(labelProxyStatus, 3, 0) gridBoxProxy.addWidget(self.labelproxyStatus, 3, 1) gridBoxProxy.addWidget(labelProxyTimeLag, 4, 0) gridBoxProxy.addWidget(self.labelproxyTimeLag, 4, 1) hboxButtonCheckProxy = QHBoxLayout() hboxButtonCheckProxy.addStretch() hboxButtonCheckProxy.addWidget(self.buttonCheckProxy) vboxProxy = QVBoxLayout() vboxProxy.addLayout(gridBoxProxy) vboxProxy.addWidget(self.textEditProxy) # vboxProxy.addStretch() vboxProxy.addLayout(hboxButtonCheckProxy) self.buttonCheckProxy.clicked.connect(self.onButtonCheckProxy) self.settingProxyPanel() self.setLayout(vboxProxy)
class MyApp(QMainWindow): def __init__(self): super().__init__() self.default_font = QFont() self.default_font.setPointSize(15) self.scores_1 = 0 self.scores_2 = 0 self.now_move = 1 self.city_now = None self.city_obj = None self.city_obj_now = None self.loader_UI() def loader_UI(self): self.setGeometry(300, 300, 600, 600) self.move_label = QLabel(self) self.move_label.setGeometry(250, 70, 120, 30) self.move_label.setFont(self.default_font) self.move_label.setText(f"Now guess {self.now_move}") self.label_scores = QLabel(self) self.label_scores.setFont(self.default_font) self.label_scores.setText("0:0") self.label_scores.move(280, 0) self.cities_list = QComboBox(self) self.cities_list.currentTextChanged.connect(self.select_city) self.cities_list.setFont(self.default_font) self.cities_list.resize(150, 30) self.cities_list.move(230, 30) self.cities_list.addItem("") self.cities_list.addItems(CITIES_DICT.keys()) self.city_obj_pic = QLabel(self) self.city_obj_pic.move(10, 30) self.city_obj_pic.resize(*IMAGE_SIZE) self.city_obj_pic.hide() self.btn_prev = QPushButton(self, text='<=') self.btn_prev.clicked.connect(self.switch_img) self.btn_prev.setGeometry(30, 560, 50, 30) self.btn_prev.id_btn = 1 self.btn_prev.hide() self.btn_next = QPushButton(self, text='=>') self.btn_next.clicked.connect(self.switch_img) self.btn_next.setGeometry(520, 560, 50, 30) self.btn_next.id_btn = 2 self.btn_next.hide() self.edit_answer = QTextEdit(self) self.edit_answer.setGeometry(245 - 30, 560, 150, 30) self.edit_answer.hide() self.btn_check = QPushButton(self, text='Check') self.btn_check.setGeometry(410 - 30, 560, 50, 30) self.btn_check.clicked.connect(self.check_ans) self.btn_check.hide() def switch_img(self): id_btn = self.sender().id_btn if id_btn == 1: self.city_obj_now -= 1 else: self.city_obj_now += 1 if self.city_obj_now < 0: self.city_obj_now = len(self.city_obj) - 1 elif self.city_obj_now == len(self.city_obj): self.city_obj_now = 0 self.set_obj_picture() def set_label_scores(self): self.label_scores.setText(f"{self.scores_1}:{self.scores_2}") def check_ans(self): ans = self.edit_answer.toPlainText() msg = QMessageBox(self) if ans == self.city_now: self.add_scores(True) msg.setText("Correct") else: self.add_scores(False) msg.setText('WRONG') msg.show() self.edit_answer.clear() def add_scores(self, add): if add is True: if self.now_move == 1: self.scores_1 += 1 else: self.scores_2 += 1 self.now_move = 1 if self.now_move == 2 else 2 self.check_win() self.set_label_scores() self.move_label.setGeometry(250, 70, 120, 30) self.move_label.setText(f"Now guess {self.now_move}") self.label_scores.show() self.cities_list.show() self.city_obj_pic.hide() self.btn_next.hide() self.btn_prev.hide() self.edit_answer.hide() self.btn_check.hide() def check_win(self): if self.scores_1 > MAX_SCORE: text = 'FIRST' elif self.scores_2 > MAX_SCORE: text = 'SECOND' else: return False msg = QMessageBox(self) msg.setText(text) msg.setFont(self.default_font) msg.show() self.close() def select_city(self, city): self.cities_list.setCurrentIndex(0) if len(city) == 0: return self.city_now = city self.city_obj = CITIES_DICT[city] random.shuffle(self.city_obj) self.city_obj_now = 0 self.move_label.setGeometry(250, 0, 120, 30) self.move_label.setText(f"Now guess {self.now_move}") self.label_scores.hide() self.cities_list.hide() self.city_obj_pic.show() self.btn_next.show() self.btn_prev.show() self.edit_answer.show() self.btn_check.show() self.set_obj_picture() def set_obj_picture(self): pic = get_map(self.city_obj[self.city_obj_now]) qpix = QPixmap() qpix.loadFromData(pic) qpix = qpix.scaled(*IMAGE_SIZE) self.city_obj_pic.setPixmap(qpix)