|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2017 - 2022 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing a widget containing various buttons for accessing |
|
8 editor actions. |
|
9 """ |
|
10 |
|
11 import contextlib |
|
12 |
|
13 from PyQt6.QtCore import pyqtSlot, Qt |
|
14 from PyQt6.QtWidgets import ( |
|
15 QWidget, QVBoxLayout, QToolButton, QFrame, QMenu, QSizePolicy, QScrollArea |
|
16 ) |
|
17 |
|
18 import UI.PixmapCache |
|
19 import Preferences |
|
20 |
|
21 from . import MarkupProviders |
|
22 |
|
23 |
|
24 class EditorButtonsWidget(QWidget): |
|
25 """ |
|
26 Class implementing a widget containing various buttons for accessing |
|
27 editor actions. |
|
28 """ |
|
29 def __init__(self, editor, parent=None): |
|
30 """ |
|
31 Constructor |
|
32 |
|
33 @param editor reference to the editor |
|
34 @type Editor |
|
35 @param parent reference to the parent widget |
|
36 @type QWidget |
|
37 """ |
|
38 super().__init__(parent) |
|
39 |
|
40 margin = 2 |
|
41 spacing = 3 |
|
42 |
|
43 self.__buttonsWidget = QWidget(self) |
|
44 |
|
45 self.__layout = QVBoxLayout(self.__buttonsWidget) |
|
46 self.__layout.setContentsMargins(0, 0, 0, 0) |
|
47 self.__layout.setSpacing(spacing) |
|
48 |
|
49 self.__provider = None |
|
50 |
|
51 self.__editor = editor |
|
52 self.__editor.languageChanged.connect(self.__updateButtonStates) |
|
53 self.__editor.editorSaved.connect(self.__updateButtonStates) |
|
54 self.__editor.editorRenamed.connect(self.__updateButtonStates) |
|
55 self.__editor.selectionChanged.connect(self.__editorSelectionChanged) |
|
56 self.__editor.settingsRead.connect(self.__editorSettingsRead) |
|
57 |
|
58 self.__createButtons() |
|
59 |
|
60 self.__layout.addStretch() |
|
61 |
|
62 self.__outerLayout = QVBoxLayout(self) |
|
63 self.__outerLayout.setContentsMargins(margin, margin, margin, margin) |
|
64 self.__outerLayout.setSpacing(spacing) |
|
65 self.__outerLayout.setAlignment(Qt.AlignmentFlag.AlignHCenter) |
|
66 |
|
67 self.__upButton = QToolButton(self) |
|
68 self.__upButton.setArrowType(Qt.ArrowType.UpArrow) |
|
69 self.__upButton.setSizePolicy( |
|
70 QSizePolicy.Policy.MinimumExpanding, QSizePolicy.Policy.Minimum) |
|
71 self.__upButton.setAutoRepeat(True) |
|
72 |
|
73 self.__scroller = QScrollArea(self) |
|
74 self.__scroller.setWidget(self.__buttonsWidget) |
|
75 self.__scroller.setSizePolicy( |
|
76 QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding) |
|
77 self.__scroller.setFrameShape(QFrame.Shape.NoFrame) |
|
78 self.__scroller.setVerticalScrollBarPolicy( |
|
79 Qt.ScrollBarPolicy.ScrollBarAlwaysOff) |
|
80 self.__scroller.setHorizontalScrollBarPolicy( |
|
81 Qt.ScrollBarPolicy.ScrollBarAlwaysOff) |
|
82 self.__scroller.setWidgetResizable(False) |
|
83 |
|
84 self.__downButton = QToolButton(self) |
|
85 self.__downButton.setArrowType(Qt.ArrowType.DownArrow) |
|
86 self.__downButton.setSizePolicy( |
|
87 QSizePolicy.Policy.MinimumExpanding, QSizePolicy.Policy.Minimum) |
|
88 self.__downButton.setAutoRepeat(True) |
|
89 |
|
90 self.__outerLayout.addWidget(self.__upButton) |
|
91 self.__outerLayout.addWidget(self.__scroller) |
|
92 self.__outerLayout.addWidget(self.__downButton) |
|
93 |
|
94 self.__upButton.clicked.connect(self.__slideUp) |
|
95 self.__downButton.clicked.connect(self.__slideDown) |
|
96 |
|
97 self.setMaximumWidth( |
|
98 self.__buttons["bold"].sizeHint().width() + 2 * margin) |
|
99 |
|
100 self.__updateButtonStates() |
|
101 |
|
102 ####################################################################### |
|
103 ## Methods below implement some event handlers |
|
104 ####################################################################### |
|
105 |
|
106 def show(self): |
|
107 """ |
|
108 Public slot to show the widget. |
|
109 """ |
|
110 super().show() |
|
111 self.__enableScrollerButtons() |
|
112 |
|
113 def resizeEvent(self, evt): |
|
114 """ |
|
115 Protected method to handle resize events. |
|
116 |
|
117 @param evt reference to the resize event (QResizeEvent) |
|
118 """ |
|
119 self.__enableScrollerButtons() |
|
120 super().resizeEvent(evt) |
|
121 |
|
122 ####################################################################### |
|
123 ## Methods below implement scroller related functions |
|
124 ####################################################################### |
|
125 |
|
126 def __enableScrollerButtons(self): |
|
127 """ |
|
128 Private method to set the enabled state of the scroll buttons. |
|
129 """ |
|
130 scrollBar = self.__scroller.verticalScrollBar() |
|
131 self.__upButton.setEnabled(scrollBar.value() > 0) |
|
132 self.__downButton.setEnabled(scrollBar.value() < scrollBar.maximum()) |
|
133 |
|
134 def __slideUp(self): |
|
135 """ |
|
136 Private slot to move the widget upwards, i.e. show contents to the |
|
137 bottom. |
|
138 """ |
|
139 self.__slide(True) |
|
140 |
|
141 def __slideDown(self): |
|
142 """ |
|
143 Private slot to move the widget downwards, i.e. show contents to |
|
144 the top. |
|
145 """ |
|
146 self.__slide(False) |
|
147 |
|
148 def __slide(self, up): |
|
149 """ |
|
150 Private method to move the sliding widget. |
|
151 |
|
152 @param up flag indicating to move upwards (boolean) |
|
153 """ |
|
154 scrollBar = self.__scroller.verticalScrollBar() |
|
155 stepSize = scrollBar.singleStep() |
|
156 if up: |
|
157 stepSize = -stepSize |
|
158 newValue = scrollBar.value() + stepSize |
|
159 if newValue < 0: |
|
160 newValue = 0 |
|
161 elif newValue > scrollBar.maximum(): |
|
162 newValue = scrollBar.maximum() |
|
163 scrollBar.setValue(newValue) |
|
164 self.__enableScrollerButtons() |
|
165 |
|
166 ####################################################################### |
|
167 ## Methods below implement the format button functions |
|
168 ####################################################################### |
|
169 |
|
170 def __createButtons(self): |
|
171 """ |
|
172 Private slot to create the various tool buttons. |
|
173 """ |
|
174 self.__buttons = {} |
|
175 self.__separators = [] |
|
176 self.__headerMenu = QMenu() |
|
177 |
|
178 self.__addButton("bold", "formatTextBold", |
|
179 self.tr("Bold")) |
|
180 self.__addButton("italic", "formatTextItalic", |
|
181 self.tr("Italic")) |
|
182 self.__addButton("strikethrough", "formatTextStrikethrough", |
|
183 self.tr("Strike Through")) |
|
184 self.__addSeparator() |
|
185 self.__addButton("header1", "formatTextHeader1", |
|
186 self.tr("Header 1")) |
|
187 self.__addButton("header2", "formatTextHeader2", |
|
188 self.tr("Header 2")) |
|
189 self.__addButton("header3", "formatTextHeader3", |
|
190 self.tr("Header 3")) |
|
191 button = self.__addButton("header", "formatTextHeader", |
|
192 self.tr("Header")) |
|
193 button.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup) |
|
194 button.setMenu(self.__headerMenu) |
|
195 self.__addSeparator() |
|
196 self.__addButton("code", "formatTextInlineCode", |
|
197 self.tr("Inline Code")) |
|
198 self.__addButton("codeBlock", "formatTextCodeBlock", |
|
199 self.tr("Code Block")) |
|
200 self.__addButton("quote", "formatTextQuote", |
|
201 self.tr("Quote")) |
|
202 self.__addSeparator() |
|
203 self.__addButton("hyperlink", "formatTextHyperlink", |
|
204 self.tr("Add Hyperlink")) |
|
205 self.__addButton("line", "formatTextHorizontalLine", |
|
206 self.tr("Add Horizontal Line")) |
|
207 self.__addButton("image", "formatTextImage", |
|
208 self.tr("Add Image")) |
|
209 self.__addSeparator() |
|
210 self.__addButton("bulletedList", "formatTextBulletedList", |
|
211 self.tr("Add Bulleted List")) |
|
212 self.__addButton("numberedList", "formatTextNumberedList", |
|
213 self.tr("Add Numbered List")) |
|
214 |
|
215 self.__headerMenu.triggered.connect(self.__headerMenuTriggered) |
|
216 |
|
217 def __addButton(self, formatName, iconName, toolTip): |
|
218 """ |
|
219 Private method to add a format button. |
|
220 |
|
221 @param formatName unique name of the format |
|
222 @type str |
|
223 @param iconName name of the icon for the button |
|
224 @type str |
|
225 @param toolTip text for the tool tip |
|
226 @type str |
|
227 @return generated button |
|
228 @rtype QToolButton |
|
229 """ |
|
230 button = QToolButton(self.__buttonsWidget) |
|
231 button.setIcon(UI.PixmapCache.getIcon(iconName)) |
|
232 button.setToolTip(toolTip) |
|
233 button.clicked.connect(lambda: self.__formatClicked(formatName)) |
|
234 self.__layout.addWidget(button) |
|
235 self.__buttons[formatName] = button |
|
236 |
|
237 return button |
|
238 |
|
239 def __addSeparator(self): |
|
240 """ |
|
241 Private method to add a separator line. |
|
242 """ |
|
243 line = QFrame(self.__buttonsWidget) |
|
244 line.setLineWidth(2) |
|
245 if isinstance(self.__layout, QVBoxLayout): |
|
246 line.setFrameShape(QFrame.Shape.HLine) |
|
247 else: |
|
248 line.setFrameShape(QFrame.Shape.VLine) |
|
249 line.setFrameShadow(QFrame.Shadow.Sunken) |
|
250 |
|
251 self.__layout.addWidget(line) |
|
252 self.__separators.append(line) |
|
253 |
|
254 @pyqtSlot() |
|
255 def __updateButtonStates(self): |
|
256 """ |
|
257 Private slot to change the button states. |
|
258 """ |
|
259 provider = MarkupProviders.getMarkupProvider(self.__editor) |
|
260 if ( |
|
261 self.__provider is None or |
|
262 provider.kind() != self.__provider.kind() |
|
263 ): |
|
264 self.__provider = provider |
|
265 |
|
266 self.__buttons["bold"].setEnabled(self.__provider.hasBold()) |
|
267 self.__buttons["italic"].setEnabled(self.__provider.hasItalic()) |
|
268 self.__buttons["strikethrough"].setEnabled( |
|
269 self.__provider.hasStrikethrough()) |
|
270 |
|
271 headerLevels = self.__provider.headerLevels() |
|
272 self.__buttons["header1"].setEnabled(headerLevels >= 1) |
|
273 self.__buttons["header2"].setEnabled(headerLevels >= 2) |
|
274 self.__buttons["header3"].setEnabled(headerLevels >= 3) |
|
275 self.__buttons["header"].setEnabled(headerLevels > 3) |
|
276 self.__headerMenu.clear() |
|
277 for level in range(1, headerLevels + 1): |
|
278 act = self.__headerMenu.addAction( |
|
279 self.tr("Level {0}").format(level)) |
|
280 act.setData("header{0}".format(level)) |
|
281 |
|
282 self.__buttons["code"].setEnabled(self.__provider.hasCode()) |
|
283 self.__buttons["codeBlock"].setEnabled( |
|
284 self.__provider.hasCodeBlock()) |
|
285 |
|
286 self.__buttons["bulletedList"].setEnabled( |
|
287 self.__provider.hasBulletedList()) |
|
288 self.__buttons["numberedList"].setEnabled( |
|
289 self.__provider.hasNumberedList()) |
|
290 |
|
291 self.__editorSelectionChanged() |
|
292 |
|
293 if Preferences.getEditor("HideFormatButtons"): |
|
294 self.setVisible(self.__provider.kind() != "none") |
|
295 |
|
296 def __formatClicked(self, formatName): |
|
297 """ |
|
298 Private slot to handle a format button being clicked. |
|
299 |
|
300 @param formatName format type of the button |
|
301 @type str |
|
302 """ |
|
303 if formatName == "bold": |
|
304 self.__provider.bold(self.__editor) |
|
305 elif formatName == "italic": |
|
306 self.__provider.italic(self.__editor) |
|
307 elif formatName == "strikethrough": |
|
308 self.__provider.strikethrough(self.__editor) |
|
309 elif formatName.startswith("header"): |
|
310 with contextlib.suppress(ValueError): |
|
311 level = int(formatName[-1]) |
|
312 self.__provider.header(self.__editor, level) |
|
313 elif formatName == "code": |
|
314 self.__provider.code(self.__editor) |
|
315 elif formatName == "codeBlock": |
|
316 self.__provider.codeBlock(self.__editor) |
|
317 elif formatName == "quote": |
|
318 self.__provider.quote(self.__editor) |
|
319 elif formatName == "hyperlink": |
|
320 self.__provider.hyperlink(self.__editor) |
|
321 elif formatName == "line": |
|
322 self.__provider.line(self.__editor) |
|
323 elif formatName == "image": |
|
324 self.__provider.image(self.__editor) |
|
325 elif formatName == "bulletedList": |
|
326 self.__provider.bulletedList(self.__editor) |
|
327 elif formatName == "numberedList": |
|
328 self.__provider.numberedList(self.__editor) |
|
329 |
|
330 def __headerMenuTriggered(self, act): |
|
331 """ |
|
332 Private method handling the selection of a header menu entry. |
|
333 |
|
334 @param act action of the headers menu that was triggered |
|
335 @type QAction |
|
336 """ |
|
337 formatName = act.data() |
|
338 self.__formatClicked(formatName) |
|
339 |
|
340 def __editorSelectionChanged(self): |
|
341 """ |
|
342 Private slot to handle a change of the editor's selection. |
|
343 """ |
|
344 hasSelection = self.__editor.hasSelectedText() |
|
345 if self.__provider: |
|
346 self.__buttons["quote"].setEnabled( |
|
347 self.__provider.hasQuote() and ( |
|
348 self.__provider.kind() == "html" or hasSelection |
|
349 ) |
|
350 ) |
|
351 self.__buttons["hyperlink"].setEnabled( |
|
352 self.__provider.hasHyperlink() and not hasSelection) |
|
353 self.__buttons["line"].setEnabled( |
|
354 self.__provider.hasLine() and not hasSelection) |
|
355 self.__buttons["image"].setEnabled( |
|
356 self.__provider.hasImage() and not hasSelection) |
|
357 |
|
358 def __editorSettingsRead(self): |
|
359 """ |
|
360 Private slot to handle a change of the editor related settings. |
|
361 """ |
|
362 if Preferences.getEditor("HideFormatButtons"): |
|
363 if self.__provider is not None: |
|
364 self.setVisible(self.__provider.kind() != "none") |
|
365 else: |
|
366 self.setVisible(True) |