1 /* This file is part of the KDE project
2 Copyright (C) 2008 Fela Winkelmolen <fela.kde@gmail.com>
3
4 This library is free software; you can redistribute it and/or
5 modify it under the terms of the GNU Library General Public
6 License as published by the Free Software Foundation; either
7 version 2 of the License, or (at your option) any later version.
8
9 This library is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY; without even the implied warranty of
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 Library General Public License for more details.
13
14 You should have received a copy of the GNU Library General Public License
15 along with this library; see the file COPYING.LIB. If not, write to
16 the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
17 Boston, MA 02110-1301, USA.
18 */
19
20 #include "KarbonCalligraphyOptionWidget.h"
21
22 #include <KoIcon.h>
23
24 #include <klocalizedstring.h>
25 #include <kcombobox.h>
26 #include <kconfiggroup.h>
27 #include <ksharedconfig.h>
28 #include <kconfig.h>
29 #include <QDebug>
30 #include <kmessagebox.h>
31 #include <KisAngleSelector.h>
32
33 #include <QInputDialog>
34 #include <QCheckBox>
35 #include <QDoubleSpinBox>
36 #include <QLabel>
37 #include <QGridLayout>
38 #include <QToolButton>
39
40 #include "kis_double_parse_spin_box.h"
41 #include "kis_int_parse_spin_box.h"
42
43 /*
44 Profiles are saved in karboncalligraphyrc
45
46 In the group "General", profile is the name of profile used
47
48 Every profile is described in a group, the name of which is "ProfileN"
49 Starting to count from 0 onwards
50 (NOTE: the index in profiles is different from the N)
51
52 Default profiles are added by the function addDefaultProfiles(), once they
53 have been added, the entry defaultProfilesAdded in the "General" group is
54 set to true
55
56 TODO: add a reset defaults option?
57 */
58
59 // name of the configuration file
60 const QString RCFILENAME = "karboncalligraphyrc";
61
KarbonCalligraphyOptionWidget()62 KarbonCalligraphyOptionWidget::KarbonCalligraphyOptionWidget()
63 : m_changingProfile(false)
64 {
65 QGridLayout *layout = new QGridLayout(this);
66 layout->setContentsMargins(0, 0, 0, 0);
67
68 m_comboBox = new KComboBox(this);
69 layout->addWidget(m_comboBox, 0, 0);
70
71 m_saveButton = new QToolButton(this);
72 m_saveButton->setToolTip(i18n("Save profile as..."));
73 m_saveButton->setIcon(koIcon("document-save-as"));
74 layout->addWidget(m_saveButton, 0, 1);
75
76 m_removeButton = new QToolButton(this);
77 m_removeButton->setToolTip(i18n("Remove profile"));
78 m_removeButton->setIcon(koIcon("list-remove"));
79 layout->addWidget(m_removeButton, 0, 2);
80
81 QGridLayout *detailsLayout = new QGridLayout();
82 detailsLayout->setContentsMargins(0, 0, 0, 0);
83 detailsLayout->setVerticalSpacing(0);
84
85 m_usePath = new QCheckBox(i18n("&Follow selected path"), this);
86 detailsLayout->addWidget(m_usePath, 0, 0, 1, 4);
87
88 m_usePressure = new QCheckBox(i18n("Use tablet &pressure"), this);
89 detailsLayout->addWidget(m_usePressure, 1, 0, 1, 4);
90
91 QLabel *widthLabel = new QLabel(i18n("Width:"), this);
92 widthLabel->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
93 m_widthBox = new KisDoubleParseSpinBox(this);
94 m_widthBox->setRange(0.0, 999.0);
95 widthLabel->setBuddy(m_widthBox);
96 detailsLayout->addWidget(widthLabel, 2, 2);
97 detailsLayout->addWidget(m_widthBox, 2, 3);
98
99 QLabel *thinningLabel = new QLabel(i18n("Thinning:"), this);
100 thinningLabel->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
101 m_thinningBox = new KisDoubleParseSpinBox(this);
102 m_thinningBox->setRange(-1.0, 1.0);
103 m_thinningBox->setSingleStep(0.1);
104 thinningLabel->setBuddy(m_thinningBox);
105 detailsLayout->addWidget(thinningLabel, 2, 0);
106 detailsLayout->addWidget(m_thinningBox, 2, 1);
107
108 m_useAngle = new QCheckBox(i18n("Use tablet &angle"), this);
109 detailsLayout->addWidget(m_useAngle, 3, 0, 1, 4);
110
111 QLabel *angleLabel = new QLabel(i18n("Angle:"), this);
112 angleLabel->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
113 m_angleBox = new KisAngleSelector(this);
114 m_angleBox->setRange(0, 179);
115 m_angleBox->setDecimals(0);
116 m_angleBox->setResetAngle(30);
117 m_angleBox->setFlipOptionsMode(KisAngleSelector::FlipOptionsMode_ContextMenu);
118 angleLabel->setBuddy(m_angleBox);
119 detailsLayout->addWidget(angleLabel, 4, 0);
120 detailsLayout->addWidget(m_angleBox, 4, 1);
121
122 QLabel *fixationLabel = new QLabel(i18n("Fixation:"), this);
123 fixationLabel->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
124 m_fixationBox = new KisDoubleParseSpinBox(this);
125 m_fixationBox->setRange(0.0, 1.0);
126 m_fixationBox->setSingleStep(0.1);
127 fixationLabel->setBuddy(m_fixationBox);
128 detailsLayout->addWidget(fixationLabel, 5, 0);
129 detailsLayout->addWidget(m_fixationBox, 5, 1);
130
131 QLabel *capsLabel = new QLabel(i18n("Caps:"), this);
132 capsLabel->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
133 m_capsBox = new KisDoubleParseSpinBox(this);
134 m_capsBox->setRange(0.0, 2.0);
135 m_capsBox->setSingleStep(0.03);
136 capsLabel->setBuddy(m_capsBox);
137 detailsLayout->addWidget(capsLabel, 5, 2);
138 detailsLayout->addWidget(m_capsBox, 5, 3);
139
140 QLabel *massLabel = new QLabel(i18n("Mass:"), this);
141 massLabel->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
142 m_massBox = new KisDoubleParseSpinBox(this);
143 m_massBox->setRange(0.0, 20.0);
144 m_massBox->setDecimals(1);
145 massLabel->setBuddy(m_massBox);
146 detailsLayout->addWidget(massLabel, 6, 0);
147 detailsLayout->addWidget(m_massBox, 6, 1);
148
149 QLabel *dragLabel = new QLabel(i18n("Drag:"), this);
150 dragLabel->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
151 m_dragBox = new KisDoubleParseSpinBox(this);
152 m_dragBox->setRange(0.0, 1.0);
153 m_dragBox->setSingleStep(0.1);
154 dragLabel->setBuddy(m_dragBox);
155 detailsLayout->addWidget(dragLabel, 6, 2);
156 detailsLayout->addWidget(m_dragBox, 6, 3);
157
158 layout->addLayout(detailsLayout, 1, 0, 1, 3);
159 layout->setRowStretch(2, 1);
160
161 createConnections();
162 addDefaultProfiles(); // if they are already added does nothing
163 loadProfiles();
164 }
165
~KarbonCalligraphyOptionWidget()166 KarbonCalligraphyOptionWidget::~KarbonCalligraphyOptionWidget()
167 {
168 qDeleteAll(m_profiles);
169 }
170
emitAll()171 void KarbonCalligraphyOptionWidget::emitAll()
172 {
173 emit usePathChanged(m_usePath->isChecked());
174 emit usePressureChanged(m_usePressure->isChecked());
175 emit useAngleChanged(m_useAngle->isChecked());
176 emit widthChanged(m_widthBox->value());
177 emit thinningChanged(m_thinningBox->value());
178 emit angleChanged(static_cast<int>(m_angleBox->angle()));
179 emit fixationChanged(m_fixationBox->value());
180 emit capsChanged(m_capsBox->value());
181 emit massChanged(m_massBox->value());
182 emit dragChanged(m_dragBox->value());
183 }
184
loadProfile(const QString & name)185 void KarbonCalligraphyOptionWidget::loadProfile(const QString &name)
186 {
187 if (m_changingProfile) {
188 return;
189 }
190 // write the new profile in the config file
191 KConfig config(RCFILENAME);
192 KConfigGroup generalGroup(&config, "General");
193 generalGroup.writeEntry("profile", name);
194 config.sync();
195
196 // and load it
197 loadCurrentProfile();
198
199 // don't show Current if it isn't selected
200 if (name != i18n("Current")) {
201 removeProfile(i18n("Current"));
202 }
203 }
204
updateCurrentProfile()205 void KarbonCalligraphyOptionWidget::updateCurrentProfile()
206 {
207 if (!m_changingProfile) {
208 saveProfile(i18n("Current"));
209 }
210 }
211
saveProfileAs()212 void KarbonCalligraphyOptionWidget::saveProfileAs()
213 {
214 QString name;
215
216 // loop until a valid name is entered or the user cancelled
217 while (1) {
218 bool ok;
219 name = QInputDialog::getText(this,
220 i18n("Profile name"),
221 i18n("Please insert the name by which "
222 "you want to save this profile:"),
223 QLineEdit::Normal, QString(), &ok);
224 if (!ok) {
225 return;
226 }
227
228 if (name.isEmpty() || name == i18n("Current")) {
229 KMessageBox::sorry(this,
230 i18n("Sorry, the name you entered is invalid."),
231 i18nc("invalid profile name", "Invalid name."));
232 // try again
233 saveProfileAs();
234 continue; // ask again
235 }
236
237 if (m_profiles.contains(name)) {
238 int ret = KMessageBox::warningYesNo(this,
239 i18n("A profile with that name already exists.\n"
240 "Do you want to overwrite it?"));
241
242 if (ret == KMessageBox::Yes) {
243 break; // exit while loop (save profile)
244 }
245 // else ask again
246 } else {
247 // the name is valid
248 break; // exit while loop (save profile)
249 }
250 }
251
252 saveProfile(name);
253 }
254
removeProfile()255 void KarbonCalligraphyOptionWidget::removeProfile()
256 {
257 removeProfile(m_comboBox->currentText());
258 }
259
toggleUseAngle(bool checked)260 void KarbonCalligraphyOptionWidget::toggleUseAngle(bool checked)
261 {
262 m_angleBox->setEnabled(! checked);
263 }
264
increaseWidth()265 void KarbonCalligraphyOptionWidget::increaseWidth()
266 {
267 m_widthBox->setValue(m_widthBox->value() + 1);
268 }
269
decreaseWidth()270 void KarbonCalligraphyOptionWidget::decreaseWidth()
271 {
272 m_widthBox->setValue(m_widthBox->value() - 1);
273 }
274
increaseAngle()275 void KarbonCalligraphyOptionWidget::increaseAngle()
276 {
277 m_angleBox->setAngle(static_cast<int>(m_angleBox->angle() + 3) % 180);
278 }
279
decreaseAngle()280 void KarbonCalligraphyOptionWidget::decreaseAngle()
281 {
282 m_angleBox->setAngle(static_cast<int>(m_angleBox->angle() - 3) % 180);
283 }
284
285 /******************************************************************************
286 ************************* Convenience Functions ******************************
287 ******************************************************************************/
288
createConnections()289 void KarbonCalligraphyOptionWidget::createConnections()
290 {
291 connect(m_comboBox, SIGNAL(currentIndexChanged(QString)),
292 SLOT(loadProfile(QString)));
293
294 // propagate changes
295 connect(m_usePath, SIGNAL(toggled(bool)),
296 SIGNAL(usePathChanged(bool)));
297
298 connect(m_usePressure, SIGNAL(toggled(bool)),
299 SIGNAL(usePressureChanged(bool)));
300
301 connect(m_useAngle, SIGNAL(toggled(bool)),
302 SIGNAL(useAngleChanged(bool)));
303
304 connect(m_widthBox, SIGNAL(valueChanged(double)),
305 SIGNAL(widthChanged(double)));
306
307 connect(m_thinningBox, SIGNAL(valueChanged(double)),
308 SIGNAL(thinningChanged(double)));
309
310 connect(m_angleBox, SIGNAL(angleChanged(qreal)),
311 SLOT(on_m_angleBox_angleChanged(qreal)));
312
313 connect(m_fixationBox, SIGNAL(valueChanged(double)),
314 SIGNAL(fixationChanged(double)));
315
316 connect(m_capsBox, SIGNAL(valueChanged(double)),
317 SIGNAL(capsChanged(double)));
318
319 connect(m_massBox, SIGNAL(valueChanged(double)),
320 SIGNAL(massChanged(double)));
321
322 connect(m_dragBox, SIGNAL(valueChanged(double)),
323 SIGNAL(dragChanged(double)));
324
325 // update profile
326 connect(m_usePath, SIGNAL(toggled(bool)),
327 SLOT(updateCurrentProfile()));
328
329 connect(m_usePressure, SIGNAL(toggled(bool)),
330 SLOT(updateCurrentProfile()));
331
332 connect(m_useAngle, SIGNAL(toggled(bool)),
333 SLOT(updateCurrentProfile()));
334
335 connect(m_widthBox, SIGNAL(valueChanged(double)),
336 SLOT(updateCurrentProfile()));
337
338 connect(m_thinningBox, SIGNAL(valueChanged(double)),
339 SLOT(updateCurrentProfile()));
340
341 connect(m_angleBox, SIGNAL(valueChanged(int)),
342 SLOT(updateCurrentProfile()));
343
344 connect(m_fixationBox, SIGNAL(valueChanged(double)),
345 SLOT(updateCurrentProfile()));
346
347 connect(m_capsBox, SIGNAL(valueChanged(double)),
348 SLOT(updateCurrentProfile()));
349
350 connect(m_massBox, SIGNAL(valueChanged(double)),
351 SLOT(updateCurrentProfile()));
352
353 connect(m_dragBox, SIGNAL(valueChanged(double)),
354 SLOT(updateCurrentProfile()));
355
356 connect(m_saveButton, SIGNAL(clicked()), SLOT(saveProfileAs()));
357 connect(m_removeButton, SIGNAL(clicked()), SLOT(removeProfile()));
358
359 // visualization
360 connect(m_useAngle, SIGNAL(toggled(bool)), SLOT(toggleUseAngle(bool)));
361 }
362
addDefaultProfiles()363 void KarbonCalligraphyOptionWidget::addDefaultProfiles()
364 {
365 // check if the profiles where already added
366 KConfig config(RCFILENAME);
367 KConfigGroup generalGroup(&config, "General");
368
369 if (generalGroup.readEntry("defaultProfilesAdded", false)) {
370 return;
371 }
372
373 KConfigGroup profile0(&config, "Profile0");
374 profile0.writeEntry("name", i18n("Mouse"));
375 profile0.writeEntry("usePath", false);
376 profile0.writeEntry("usePressure", false);
377 profile0.writeEntry("useAngle", false);
378 profile0.writeEntry("width", 30.0);
379 profile0.writeEntry("thinning", 0.2);
380 profile0.writeEntry("angle", 30);
381 profile0.writeEntry("fixation", 1.0);
382 profile0.writeEntry("caps", 0.0);
383 profile0.writeEntry("mass", 3.0);
384 profile0.writeEntry("drag", 0.7);
385
386 KConfigGroup profile1(&config, "Profile1");
387 profile1.writeEntry("name", i18n("Graphics Pen"));
388 profile1.writeEntry("width", 50.0);
389 profile1.writeEntry("usePath", false);
390 profile1.writeEntry("usePressure", false);
391 profile1.writeEntry("useAngle", false);
392 profile1.writeEntry("thinning", 0.2);
393 profile1.writeEntry("angle", 30);
394 profile1.writeEntry("fixation", 1.0);
395 profile1.writeEntry("caps", 0.0);
396 profile1.writeEntry("mass", 1.0);
397 profile1.writeEntry("drag", 0.9);
398
399 generalGroup.writeEntry("profile", i18n("Mouse"));
400 generalGroup.writeEntry("defaultProfilesAdded", true);
401
402 config.sync();
403 }
404
loadProfiles()405 void KarbonCalligraphyOptionWidget::loadProfiles()
406 {
407 KConfig config(RCFILENAME);
408
409 // load profiles as long as they are present
410 int i = 0;
411 while (1) { // forever
412 KConfigGroup profileGroup(&config, "Profile" + QString::number(i));
413 // invalid profile, assume we reached the last one
414 if (!profileGroup.hasKey("name")) {
415 break;
416 }
417
418 Profile *profile = new Profile;
419 profile->index = i;
420 profile->name = profileGroup.readEntry("name", QString());
421 profile->usePath = profileGroup.readEntry("usePath", false);
422 profile->usePressure = profileGroup.readEntry("usePressure", false);
423 profile->useAngle = profileGroup.readEntry("useAngle", false);
424 profile->width = profileGroup.readEntry("width", 30.0);
425 profile->thinning = profileGroup.readEntry("thinning", 0.2);
426 profile->angle = profileGroup.readEntry("angle", 30);
427 profile->fixation = profileGroup.readEntry("fixation", 0.0);
428 profile->caps = profileGroup.readEntry("caps", 0.0);
429 profile->mass = profileGroup.readEntry("mass", 3.0);
430 profile->drag = profileGroup.readEntry("drag", 0.7);
431
432 m_profiles.insert(profile->name, profile);
433 ++i;
434 }
435
436 m_changingProfile = true;
437 ProfileMap::const_iterator it = m_profiles.constBegin();
438 ProfileMap::const_iterator lastIt = m_profiles.constEnd();
439 for (; it != lastIt; ++it) {
440 m_comboBox->addItem(it.key());
441 }
442 m_changingProfile = false;
443
444 loadCurrentProfile();
445 }
446
loadCurrentProfile()447 void KarbonCalligraphyOptionWidget::loadCurrentProfile()
448 {
449 KConfig config(RCFILENAME);
450 KConfigGroup generalGroup(&config, "General");
451 QString currentProfile = generalGroup.readEntry("profile", QString());
452 // find the index needed by the comboBox
453 int index = profilePosition(currentProfile);
454
455 if (currentProfile.isEmpty() || index < 0) {
456 return;
457 }
458
459 m_comboBox->setCurrentIndex(index);
460
461 Profile *profile = m_profiles[currentProfile];
462
463 m_changingProfile = true;
464 m_usePath->setChecked(profile->usePath);
465 m_usePressure->setChecked(profile->usePressure);
466 m_useAngle->setChecked(profile->useAngle);
467 m_widthBox->setValue(profile->width);
468 m_thinningBox->setValue(profile->thinning);
469 m_angleBox->setAngle(profile->angle);
470 m_fixationBox->setValue(profile->fixation);
471 m_capsBox->setValue(profile->caps);
472 m_massBox->setValue(profile->mass);
473 m_dragBox->setValue(profile->drag);
474 m_changingProfile = false;
475 }
476
saveProfile(const QString & name)477 void KarbonCalligraphyOptionWidget::saveProfile(const QString &name)
478 {
479 Profile *profile = new Profile;
480 profile->name = name;
481 profile->usePath = m_usePath->isChecked();
482 profile->usePressure = m_usePressure->isChecked();
483 profile->useAngle = m_useAngle->isChecked();
484 profile->width = m_widthBox->value();
485 profile->thinning = m_thinningBox->value();
486 profile->angle = static_cast<int>(m_angleBox->angle());
487 profile->fixation = m_fixationBox->value();
488 profile->caps = m_capsBox->value();
489 profile->mass = m_massBox->value();
490 profile->drag = m_dragBox->value();
491
492 if (m_profiles.contains(name)) {
493 // there is already a profile with the same name, overwrite
494 profile->index = m_profiles[name]->index;
495 m_profiles.insert(name, profile);
496 } else {
497 // it is a new profile
498 profile->index = m_profiles.count();
499 m_profiles.insert(name, profile);
500 // add the profile to the combobox
501 QString dbg;
502 for (int i = 0; i < m_comboBox->count(); ++i) {
503 dbg += m_comboBox->itemText(i) + ' ';
504 }
505 int pos = profilePosition(name);
506 m_changingProfile = true;
507 m_comboBox->insertItem(pos, name);
508 m_changingProfile = false;
509 for (int i = 0; i < m_comboBox->count(); ++i) {
510 dbg += m_comboBox->itemText(i) + ' ';
511 }
512 }
513
514 KConfig config(RCFILENAME);
515 QString str = "Profile" + QString::number(profile->index);
516 KConfigGroup profileGroup(&config, str);
517
518 profileGroup.writeEntry("name", name);
519 profileGroup.writeEntry("usePath", profile->usePath);
520 profileGroup.writeEntry("usePressure", profile->usePressure);
521 profileGroup.writeEntry("useAngle", profile->useAngle);
522 profileGroup.writeEntry("width", profile->width);
523 profileGroup.writeEntry("thinning", profile->thinning);
524 profileGroup.writeEntry("angle", profile->angle);
525 profileGroup.writeEntry("fixation", profile->fixation);
526 profileGroup.writeEntry("caps", profile->caps);
527 profileGroup.writeEntry("mass", profile->mass);
528 profileGroup.writeEntry("drag", profile->drag);
529
530 KConfigGroup generalGroup(&config, "General");
531 generalGroup.writeEntry("profile", name);
532
533 config.sync();
534
535 m_comboBox->setCurrentIndex(profilePosition(name));
536 }
537
removeProfile(const QString & name)538 void KarbonCalligraphyOptionWidget::removeProfile(const QString &name)
539 {
540 int index = profilePosition(name);
541 if (index < 0) {
542 return; // no such profile
543 }
544
545 // remove the file from the config file
546 KConfig config(RCFILENAME);
547 int deletedIndex = m_profiles[name]->index;
548 QString deletedGroup = "Profile" + QString::number(deletedIndex);
549 config.deleteGroup(deletedGroup);
550 config.sync();
551
552 // and from profiles
553 m_profiles.remove(name);
554
555 m_comboBox->removeItem(index);
556
557 // now in the config file there is value ProfileN missing,
558 // where N = configIndex, so put the last one there
559 if (m_profiles.isEmpty()) {
560 return;
561 }
562
563 int lastN = -1;
564 Profile *profile = 0; // profile to be moved, will be the last one
565 Q_FOREACH (Profile *p, m_profiles) {
566 if (p->index > lastN) {
567 lastN = p->index;
568 profile = p;
569 }
570 }
571
572 Q_ASSERT(profile != 0);
573
574 // do nothing if the deleted group was the last one
575 if (deletedIndex > lastN) {
576 return;
577 }
578
579 QString lastGroup = "Profile" + QString::number(lastN);
580 config.deleteGroup(lastGroup);
581
582 KConfigGroup profileGroup(&config, deletedGroup);
583 profileGroup.writeEntry("name", profile->name);
584 profileGroup.writeEntry("usePath", profile->usePath);
585 profileGroup.writeEntry("usePressure", profile->usePressure);
586 profileGroup.writeEntry("useAngle", profile->useAngle);
587 profileGroup.writeEntry("width", profile->width);
588 profileGroup.writeEntry("thinning", profile->thinning);
589 profileGroup.writeEntry("angle", profile->angle);
590 profileGroup.writeEntry("fixation", profile->fixation);
591 profileGroup.writeEntry("caps", profile->caps);
592 profileGroup.writeEntry("mass", profile->mass);
593 profileGroup.writeEntry("drag", profile->drag);
594 config.sync();
595
596 profile->index = deletedIndex;
597 }
598
profilePosition(const QString & profileName)599 int KarbonCalligraphyOptionWidget::profilePosition(const QString &profileName)
600 {
601 int res = 0;
602 ProfileMap::const_iterator it = m_profiles.constBegin();
603 ProfileMap::const_iterator lastIt = m_profiles.constEnd();
604 for (; it != lastIt; ++it) {
605 if (it.key() == profileName) {
606 return res;
607 }
608 ++res;
609 }
610 return -1;
611 }
612
setUsePathEnabled(bool enabled)613 void KarbonCalligraphyOptionWidget::setUsePathEnabled(bool enabled)
614 {
615 m_usePath->setEnabled(enabled);
616 }
617
on_m_angleBox_angleChanged(qreal angle)618 void KarbonCalligraphyOptionWidget::on_m_angleBox_angleChanged(qreal angle)
619 {
620 emit angleChanged(static_cast<int>(angle));
621 }
622