From ed51341bec119d4c7c5d019e0fd8a6d184f7b877 Mon Sep 17 00:00:00 2001 From: Friedemann Kleint Date: Fri, 16 Sep 2022 13:19:47 +0200 Subject: Add the character map example Task-number: PYSIDE-841 Change-Id: I1aa30657b904d3814c21f16c2404e057e754a960 Reviewed-by: Cristian Maureira-Fredes --- .../widgets/charactermap/charactermap.pyproject | 4 + .../widgets/charactermap/characterwidget.py | 133 ++++++++++++++++ .../widgets/charactermap/doc/charactermap.rst | 8 + .../widgets/widgets/charactermap/fontinfodialog.py | 47 ++++++ examples/widgets/widgets/charactermap/main.py | 17 +++ .../widgets/widgets/charactermap/mainwindow.py | 167 +++++++++++++++++++++ 6 files changed, 376 insertions(+) create mode 100644 examples/widgets/widgets/charactermap/charactermap.pyproject create mode 100644 examples/widgets/widgets/charactermap/characterwidget.py create mode 100644 examples/widgets/widgets/charactermap/doc/charactermap.rst create mode 100644 examples/widgets/widgets/charactermap/fontinfodialog.py create mode 100644 examples/widgets/widgets/charactermap/main.py create mode 100644 examples/widgets/widgets/charactermap/mainwindow.py (limited to 'examples/widgets') diff --git a/examples/widgets/widgets/charactermap/charactermap.pyproject b/examples/widgets/widgets/charactermap/charactermap.pyproject new file mode 100644 index 000000000..c2b2c2068 --- /dev/null +++ b/examples/widgets/widgets/charactermap/charactermap.pyproject @@ -0,0 +1,4 @@ +{ + "files": ["main.py", "characterwidget.py", "fontinfodialog.py", + "mainwindow.py"] +} diff --git a/examples/widgets/widgets/charactermap/characterwidget.py b/examples/widgets/widgets/charactermap/characterwidget.py new file mode 100644 index 000000000..1df2a3b74 --- /dev/null +++ b/examples/widgets/widgets/charactermap/characterwidget.py @@ -0,0 +1,133 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +from textwrap import dedent + +from PySide6.QtCore import QSize, Qt, Slot, Signal +from PySide6.QtGui import (QBrush, QFont, QFontDatabase, QFontMetrics, + QPainter, QPen) +from PySide6.QtWidgets import QToolTip, QWidget + +COLUMNS = 16 + + +class CharacterWidget(QWidget): + + character_selected = Signal(str) + + def __init__(self, parent=None): + super().__init__(parent) + + self._display_font = QFont() + self._last_key = -1 + self._square_size = int(0) + + self.calculate_square_size() + self.setMouseTracking(True) + + @Slot(QFont) + def update_font(self, font): + self._display_font.setFamily(font.family()) + self.calculate_square_size() + self.adjustSize() + self.update() + + @Slot(str) + def update_size(self, fontSize): + self._display_font.setPointSize(int(fontSize)) + self.calculate_square_size() + self.adjustSize() + self.update() + + @Slot(str) + def update_style(self, fontStyle): + old_strategy = self._display_font.styleStrategy() + self._display_font = QFontDatabase.font(self._display_font.family(), + fontStyle, + self._display_font.pointSize()) + self._display_font.setStyleStrategy(old_strategy) + self.calculate_square_size() + self.adjustSize() + self.update() + + @Slot(bool) + def update_font_merging(self, enable): + if enable: + self._display_font.setStyleStrategy(QFont.PreferDefault) + else: + self._display_font.setStyleStrategy(QFont.NoFontMerging) + self.adjustSize() + self.update() + + def calculate_square_size(self): + h = QFontMetrics(self._display_font, self).height() + self._square_size = max(16, 4 + h) + + def sizeHint(self): + return QSize(COLUMNS * self._square_size, + (65536 / COLUMNS) * self._square_size) + + def _unicode_from_pos(self, point): + row = int(point.y() / self._square_size) + return row * COLUMNS + int(point.x() / self._square_size) + + def mouseMoveEvent(self, event): + widget_position = self.mapFromGlobal(event.globalPosition().toPoint()) + key = self._unicode_from_pos(widget_position) + c = chr(key) + family = self._display_font.family() + text = dedent(f''' +

Character: + {c}

Value: 0x{key:x} + ''') + QToolTip.showText(event.globalPosition().toPoint(), text, self) + + def mousePressEvent(self, event): + if event.button() == Qt.LeftButton: + self._last_key = self._unicode_from_pos(event.position().toPoint()) + if self._last_key != -1: + c = chr(self._last_key) + self.character_selected.emit(f"{c}") + self.update() + else: + super().mousePressEvent(event) + + def paintEvent(self, event): + with QPainter(self) as painter: + self.render(event, painter) + + def render(self, event, painter): + painter = QPainter(self) + painter.fillRect(event.rect(), QBrush(Qt.white)) + painter.setFont(self._display_font) + redraw_rect = event.rect() + begin_row = int(redraw_rect.top() / self._square_size) + end_row = int(redraw_rect.bottom() / self._square_size) + begin_column = int(redraw_rect.left() / self._square_size) + end_column = int(redraw_rect.right() / self._square_size) + painter.setPen(QPen(Qt.gray)) + for row in range(begin_row, end_row + 1): + for column in range(begin_column, end_column + 1): + x = int(column * self._square_size) + y = int(row * self._square_size) + painter.drawRect(x, y, self._square_size, self._square_size) + + font_metrics = QFontMetrics(self._display_font) + painter.setPen(QPen(Qt.black)) + for row in range(begin_row, end_row + 1): + for column in range(begin_column, end_column + 1): + key = int(row * COLUMNS + column) + painter.setClipRect(column * self._square_size, + row * self._square_size, + self._square_size, self._square_size) + + if key == self._last_key: + painter.fillRect(column * self._square_size + 1, + row * self._square_size + 1, + self._square_size, self._square_size, QBrush(Qt.red)) + + text = chr(key) + painter.drawText(column * self._square_size + (self._square_size / 2) - + font_metrics.horizontalAdvance(text) / 2, + row * self._square_size + 4 + font_metrics.ascent(), + text) diff --git a/examples/widgets/widgets/charactermap/doc/charactermap.rst b/examples/widgets/widgets/charactermap/doc/charactermap.rst new file mode 100644 index 000000000..1a38615c4 --- /dev/null +++ b/examples/widgets/widgets/charactermap/doc/charactermap.rst @@ -0,0 +1,8 @@ +Character Map Example +===================== + +The example displays an array of characters which the user can click on +to enter text in a line edit. The contents of the line edit can then be +copied into the clipboard, and pasted into other applications. The +purpose behind this sort of tool is to allow users to enter characters +that may be unavailable or difficult to locate on their keyboards. diff --git a/examples/widgets/widgets/charactermap/fontinfodialog.py b/examples/widgets/widgets/charactermap/fontinfodialog.py new file mode 100644 index 000000000..aa874884f --- /dev/null +++ b/examples/widgets/widgets/charactermap/fontinfodialog.py @@ -0,0 +1,47 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +from PySide6.QtCore import Qt, qVersion, qFuzzyCompare +from PySide6.QtGui import QGuiApplication, QFontDatabase +from PySide6.QtWidgets import (QDialog, QDialogButtonBox, + QPlainTextEdit, QVBoxLayout) + + +def _format_font(font): + family = font.family() + size = font.pointSizeF() + return f"{family}, {size}pt" + + +class FontInfoDialog(QDialog): + + def __init__(self, parent): + super().__init__(parent) + self.setWindowFlags(self.windowFlags() & ~Qt.WindowContextHelpButtonHint) + main_layout = QVBoxLayout(self) + text_edit = QPlainTextEdit(self.text(), self) + text_edit.setReadOnly(True) + text_edit.setFont(QFontDatabase.systemFont(QFontDatabase.FixedFont)) + main_layout.addWidget(text_edit) + button_box = QDialogButtonBox(QDialogButtonBox.Close, self) + button_box.rejected.connect(self.reject) + main_layout.addWidget(button_box) + + def text(self): + default_font = QFontDatabase.systemFont(QFontDatabase.GeneralFont) + fixed_font = QFontDatabase.systemFont(QFontDatabase.FixedFont) + title_font = QFontDatabase.systemFont(QFontDatabase.TitleFont) + smallest_readable_font = QFontDatabase.systemFont(QFontDatabase.SmallestReadableFont) + + v = qVersion() + platform = QGuiApplication.platformName() + dpi = self.logicalDpiX() + dpr = self.devicePixelRatio() + text = f"Qt {v} on {platform}, {dpi}DPI" + if not qFuzzyCompare(dpr, float(1)): + text += f", device pixel ratio: {dpr}" + text += ("\n\nDefault font : " + _format_font(default_font) + + "\nFixed font : " + _format_font(fixed_font) + + "\nTitle font : " + _format_font(title_font) + + "\nSmallest font: " + _format_font(smallest_readable_font)) + return text diff --git a/examples/widgets/widgets/charactermap/main.py b/examples/widgets/widgets/charactermap/main.py new file mode 100644 index 000000000..e84a1d8af --- /dev/null +++ b/examples/widgets/widgets/charactermap/main.py @@ -0,0 +1,17 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import sys + +from PySide6.QtWidgets import QApplication + +from mainwindow import MainWindow + +"""PySide6 port of the widgets/widgets/ charactermap example from Qt6""" + + +if __name__ == "__main__": + app = QApplication(sys.argv) + window = MainWindow() + window.show() + sys.exit(app.exec()) diff --git a/examples/widgets/widgets/charactermap/mainwindow.py b/examples/widgets/widgets/charactermap/mainwindow.py new file mode 100644 index 000000000..5f0e2bce4 --- /dev/null +++ b/examples/widgets/widgets/charactermap/mainwindow.py @@ -0,0 +1,167 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +from PySide6.QtCore import Qt, QSignalBlocker, Slot +from PySide6.QtGui import QGuiApplication, QClipboard, QFont, QFontDatabase +from PySide6.QtWidgets import (QCheckBox, QComboBox, QFontComboBox, + QHBoxLayout, QLabel, QLineEdit, QMainWindow, + QPushButton, QScrollArea, + QVBoxLayout, QWidget) + +from characterwidget import CharacterWidget +from fontinfodialog import FontInfoDialog + + +class MainWindow(QMainWindow): + + def __init__(self, parent=None): + super().__init__(parent) + + self._character_widget = CharacterWidget() + self._filter_combo = QComboBox() + self._style_combo = QComboBox() + self._size_combo = QComboBox() + self._font_combo = QFontComboBox() + self._line_edit = QLineEdit() + self._scroll_area = QScrollArea() + self._font_merging = QCheckBox() + + file_menu = self.menuBar().addMenu("File") + file_menu.addAction("Quit", self.close) + help_menu = self.menuBar().addMenu("Help") + help_menu.addAction("Show Font Info", self.show_info) + help_menu.addAction("About &Qt", qApp.aboutQt) + + central_widget = QWidget() + + self._filter_label = QLabel("Filter:") + self._filter_combo = QComboBox() + self._filter_combo.addItem("All", int(QFontComboBox.AllFonts.value)) + self._filter_combo.addItem("Scalable", int(QFontComboBox.ScalableFonts.value)) + self._filter_combo.addItem("Monospaced", int(QFontComboBox.MonospacedFonts.value)) + self._filter_combo.addItem("Proportional", int(QFontComboBox.ProportionalFonts.value)) + self._filter_combo.setCurrentIndex(0) + self._filter_combo.currentIndexChanged.connect(self.filter_changed) + + self._font_label = QLabel("Font:") + self._font_combo = QFontComboBox() + self._size_label = QLabel("Size:") + self._size_combo = QComboBox() + self._style_label = QLabel("Style:") + self._style_combo = QComboBox() + self._font_merging_label = QLabel("Automatic Font Merging:") + self._font_merging = QCheckBox() + self._font_merging.setChecked(True) + + self._scroll_area = QScrollArea() + self._character_widget = CharacterWidget() + self._scroll_area.setWidget(self._character_widget) + self.find_styles(self._font_combo.currentFont()) + self.find_sizes(self._font_combo.currentFont()) + + self._line_edit = QLineEdit() + self._line_edit.setClearButtonEnabled(True) + self._clipboard_button = QPushButton("To clipboard") + self._font_combo.currentFontChanged.connect(self.find_styles) + self._font_combo.currentFontChanged.connect(self.find_sizes) + self._font_combo.currentFontChanged.connect(self._character_widget.update_font) + self._size_combo.currentTextChanged.connect(self._character_widget.update_size) + self._style_combo.currentTextChanged.connect(self._character_widget.update_style) + self._character_widget.character_selected.connect(self.insert_character) + + self._clipboard_button.clicked.connect(self.update_clipboard) + self._font_merging.toggled.connect(self._character_widget.update_font_merging) + + controls_layout = QHBoxLayout() + controls_layout.addWidget(self._filter_label) + controls_layout.addWidget(self._filter_combo, 1) + controls_layout.addWidget(self._font_label) + controls_layout.addWidget(self._font_combo, 1) + controls_layout.addWidget(self._size_label) + controls_layout.addWidget(self._size_combo, 1) + controls_layout.addWidget(self._style_label) + controls_layout.addWidget(self._style_combo, 1) + controls_layout.addWidget(self._font_merging_label) + controls_layout.addWidget(self._font_merging, 1) + controls_layout.addStretch(1) + + line_layout = QHBoxLayout() + line_layout.addWidget(self._line_edit, 1) + line_layout.addSpacing(12) + line_layout.addWidget(self._clipboard_button) + + central_layout = QVBoxLayout(central_widget) + central_layout.addLayout(controls_layout) + central_layout.addWidget(self._scroll_area, 1) + central_layout.addSpacing(4) + central_layout.addLayout(line_layout) + + self.setCentralWidget(central_widget) + self.setWindowTitle("Character Map") + + @Slot(QFont) + def find_styles(self, font): + current_item = self._style_combo.currentText() + self._style_combo.clear() + styles = QFontDatabase.styles(font.family()) + for style in styles: + self._style_combo.addItem(style) + + style_index = self._style_combo.findText(current_item) + + if style_index == -1: + self._style_combo.setCurrentIndex(0) + else: + self._style_combo.setCurrentIndex(style_index) + + @Slot(int) + def filter_changed(self, f): + filter = QFontComboBox.FontFilter(self._filter_combo.itemData(f)) + self._font_combo.setFontFilters(filter) + count = self._font_combo.count() + self.statusBar().showMessage(f"{count} font(s) found") + + @Slot(QFont) + def find_sizes(self, font): + current_size = self._size_combo.currentText() + with QSignalBlocker(self._size_combo): + # sizeCombo signals are now blocked until end of scope + self._size_combo.clear() + + style = QFontDatabase.styleString(font) + if QFontDatabase.isSmoothlyScalable(font.family(), style): + sizes = QFontDatabase.standardSizes() + for size in sizes: + self._size_combo.addItem(f"{size}") + self._size_combo.setEditable(True) + else: + sizes = QFontDatabase.smoothSizes(font.family(), style) + for size in sizes: + self._size_combo.addItem(f"{size}") + self._size_combo.setEditable(False) + + size_index = self._size_combo.findText(current_size) + + if size_index == -1: + self._size_combo.setCurrentIndex(max(0, self._size_combo.count() / 3)) + else: + self._size_combo.setCurrentIndex(size_index) + + @Slot(str) + def insert_character(self, character): + self._line_edit.insert(character) + + @Slot() + def update_clipboard(self): + clipboard = QGuiApplication.clipboard() + clipboard.setText(self._line_edit.text(), QClipboard.Clipboard) + clipboard.setText(self._line_edit.text(), QClipboard.Selection) + + @Slot() + def show_info(self): + screen_geometry = self.screen().geometry() + dialog = FontInfoDialog(self) + dialog.setWindowTitle("Fonts") + dialog.setAttribute(Qt.WA_DeleteOnClose) + dialog.resize(screen_geometry.width() / 4, screen_geometry.height() / 4) + dialog.show() -- cgit v1.2.3