FreeCAD

Форк
0
/
WidgetFactory.cpp 
751 строка · 21.6 Кб
1
/***************************************************************************
2
 *   Copyright (c) 2004 Werner Mayer <wmayer[at]users.sourceforge.net>     *
3
 *                                                                         *
4
 *   This file is part of the FreeCAD CAx development system.              *
5
 *                                                                         *
6
 *   This library is free software; you can redistribute it and/or         *
7
 *   modify it under the terms of the GNU Library General Public           *
8
 *   License as published by the Free Software Foundation; either          *
9
 *   version 2 of the License, or (at your option) any later version.      *
10
 *                                                                         *
11
 *   This library  is distributed in the hope that it will be useful,      *
12
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
13
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
14
 *   GNU Library General Public License for more details.                  *
15
 *                                                                         *
16
 *   You should have received a copy of the GNU Library General Public     *
17
 *   License along with this library; see the file COPYING.LIB. If not,    *
18
 *   write to the Free Software Foundation, Inc., 59 Temple Place,         *
19
 *   Suite 330, Boston, MA  02111-1307, USA                                *
20
 *                                                                         *
21
 ***************************************************************************/
22

23

24
#include "PreCompiled.h"
25
#ifndef _PreComp_
26
# include <QApplication>
27
# include <QVBoxLayout>
28
#endif
29

30
#ifdef FC_OS_WIN32
31
#undef max
32
#undef min
33
#ifdef _MSC_VER
34
#pragma warning( disable : 4099 )
35
#pragma warning( disable : 4522 )
36
#endif
37
#endif
38

39
#include <App/Application.h>
40
#include <Base/Console.h>
41
#include <Base/Exception.h>
42
#include <Base/Interpreter.h>
43

44
#include "WidgetFactory.h"
45
#include "PrefWidgets.h"
46
#include "PythonWrapper.h"
47
#include "UiLoader.h"
48

49

50
using namespace Gui;
51

52
Gui::WidgetFactoryInst* Gui::WidgetFactoryInst::_pcSingleton = nullptr;
53

54
WidgetFactoryInst& WidgetFactoryInst::instance()
55
{
56
    if (!_pcSingleton)
57
        _pcSingleton = new WidgetFactoryInst;
58
    return *_pcSingleton;
59
}
60

61
void WidgetFactoryInst::destruct ()
62
{
63
    if (_pcSingleton)
64
        delete _pcSingleton;
65
    _pcSingleton = nullptr;
66
}
67

68
/**
69
 * Creates a widget with the name \a sName which is a child of \a parent.
70
 * To create an instance of this widget once it must has been registered.
71
 * If there is no appropriate widget registered nullptr is returned.
72
 */
73
QWidget* WidgetFactoryInst::createWidget (const char* sName, QWidget* parent) const
74
{
75
    auto w = static_cast<QWidget*>(Produce(sName));
76

77
    // this widget class is not registered
78
    if (!w) {
79
#ifdef FC_DEBUG
80
        Base::Console().Warning("\"%s\" is not registered\n", sName);
81
#else
82
        Base::Console().Log("\"%s\" is not registered\n", sName);
83
#endif
84
        return nullptr;
85
    }
86

87
    try {
88
#ifdef FC_DEBUG
89
        const char* cName = dynamic_cast<QWidget*>(w)->metaObject()->className();
90
        Base::Console().Log("Widget of type '%s' created.\n", cName);
91
#endif
92
    }
93
    catch (...) {
94
#ifdef FC_DEBUG
95
        Base::Console().Error("%s does not inherit from \"QWidget\"\n", sName);
96
#else
97
        Base::Console().Log("%s does not inherit from \"QWidget\"\n", sName);
98
#endif
99
        delete w;
100
        return nullptr;
101
    }
102

103
    // set the parent to the widget
104
    if (parent)
105
        w->setParent(parent);
106

107
    return w;
108
}
109

110
/**
111
 * Creates a widget with the name \a sName which is a child of \a parent.
112
 * To create an instance of this widget once it must has been registered.
113
 * If there is no appropriate widget registered nullptr is returned.
114
 */
115
Gui::Dialog::PreferencePage* WidgetFactoryInst::createPreferencePage (const char* sName, QWidget* parent) const
116
{
117
    auto w = (Gui::Dialog::PreferencePage*)Produce(sName);
118

119
    // this widget class is not registered
120
    if (!w) {
121
#ifdef FC_DEBUG
122
        Base::Console().Warning("Cannot create an instance of \"%s\"\n", sName);
123
#else
124
        Base::Console().Log("Cannot create an instance of \"%s\"\n", sName);
125
#endif
126
        return nullptr;
127
    }
128

129
    if (qobject_cast<Gui::Dialog::PreferencePage*>(w)) {
130
#ifdef FC_DEBUG
131
        Base::Console().Log("Preference page of type '%s' created.\n", w->metaObject()->className());
132
#endif
133
    }
134
    else {
135
#ifdef FC_DEBUG
136
        Base::Console().Error("%s does not inherit from 'Gui::Dialog::PreferencePage'\n", sName);
137
#endif
138
        delete w;
139
        return nullptr;
140
    }
141

142
    // set the parent to the widget
143
    if (parent)
144
        w->setParent(parent);
145

146
    return w;
147
}
148

149
/**
150
 * Creates a preference widget with the name \a sName and the preference name \a sPref
151
 * which is a child of \a parent.
152
 * To create an instance of this widget once it must has been registered.
153
 * If there is no appropriate widget registered nullptr is returned.
154
 * After creation of this widget its recent preferences are restored automatically.
155
 */
156
QWidget* WidgetFactoryInst::createPrefWidget(const char* sName, QWidget* parent, const char* sPref)
157
{
158
    QWidget* w = createWidget(sName);
159
    // this widget class is not registered
160
    if (!w)
161
        return nullptr; // no valid QWidget object
162

163
    // set the parent to the widget
164
    w->setParent(parent);
165

166
    try {
167
        auto pw = dynamic_cast<PrefWidget*>(w);
168
        if (pw) {
169
            pw->setEntryName(sPref);
170
            pw->restorePreferences();
171
        }
172
    }
173
    catch (...) {
174
#ifdef FC_DEBUG
175
        Base::Console().Error("%s does not inherit from \"PrefWidget\"\n", w->metaObject()->className());
176
#endif
177
        delete w;
178
        return nullptr;
179
    }
180

181
    return w;
182
}
183

184
// ----------------------------------------------------
185

186
WidgetFactorySupplier* WidgetFactorySupplier::_pcSingleton = nullptr;
187

188
WidgetFactorySupplier & WidgetFactorySupplier::instance()
189
{
190
    // not initialized?
191
    if (!_pcSingleton)
192
        _pcSingleton = new WidgetFactorySupplier;
193
    return *_pcSingleton;
194
}
195

196
void WidgetFactorySupplier::destruct()
197
{
198
    // delete the widget factory and all its producers first
199
    WidgetFactoryInst::destruct();
200
    delete _pcSingleton;
201
    _pcSingleton=nullptr;
202
}
203

204
// ----------------------------------------------------
205

206
PrefPageUiProducer::PrefPageUiProducer (const char* filename, const char* group)
207
  : fn(QString::fromUtf8(filename))
208
{
209
    WidgetFactoryInst::instance().AddProducer(filename, this);
210
    Gui::Dialog::DlgPreferencesImp::addPage(filename, group);
211
}
212

213
PrefPageUiProducer::~PrefPageUiProducer() = default;
214

215
void* PrefPageUiProducer::Produce () const
216
{
217
    QWidget* page = new Gui::Dialog::PreferenceUiForm(fn);
218
    return static_cast<void*>(page);
219
}
220

221
// ----------------------------------------------------
222

223
PrefPagePyProducer::PrefPagePyProducer (const Py::Object& p, const char* group)
224
  : type(p)
225
{
226
    std::string str;
227
    Base::PyGILStateLocker lock;
228
    if (type.hasAttr("__name__")) {
229
        str = static_cast<std::string>(Py::String(type.getAttr("__name__")));
230
    }
231

232
    WidgetFactoryInst::instance().AddProducer(str.c_str(), this);
233
    Gui::Dialog::DlgPreferencesImp::addPage(str, group);
234
}
235

236
PrefPagePyProducer::~PrefPagePyProducer ()
237
{
238
    Base::PyGILStateLocker lock;
239
    type = Py::None();
240
}
241

242
void* PrefPagePyProducer::Produce () const
243
{
244
    Base::PyGILStateLocker lock;
245
    try {
246
        Py::Callable method(type);
247
        Py::Tuple args;
248
        Py::Object page = method.apply(args);
249
        QWidget* widget = new Gui::Dialog::PreferencePagePython(page);
250
        if (!widget->layout()) {
251
            delete widget;
252
            widget = nullptr;
253
        }
254
        return widget;
255
    }
256
    catch (Py::Exception&) {
257
        PyErr_Print();
258
        return nullptr;
259
    }
260
}
261

262
// ----------------------------------------------------
263

264
using namespace Gui::Dialog;
265

266
PreferencePagePython::PreferencePagePython(const Py::Object& p, QWidget* parent)
267
  : PreferencePage(parent), page(p)
268
{
269
    Base::PyGILStateLocker lock;
270
    Gui::PythonWrapper wrap;
271
    if (wrap.loadCoreModule()) {
272

273
        // old style class must have a form attribute while
274
        // new style classes can be the widget itself
275
        Py::Object widget;
276
        if (page.hasAttr(std::string("form")))
277
            widget = page.getAttr(std::string("form"));
278
        else
279
            widget = page;
280

281
        QObject* object = wrap.toQObject(widget);
282
        if (object) {
283
            QWidget* form = qobject_cast<QWidget*>(object);
284
            if (form) {
285
                this->setWindowTitle(form->windowTitle());
286
                auto layout = new QVBoxLayout;
287
                layout->addWidget(form);
288
                setLayout(layout);
289
            }
290
        }
291
    }
292
}
293

294
PreferencePagePython::~PreferencePagePython()
295
{
296
    Base::PyGILStateLocker lock;
297
    page = Py::None();
298
}
299

300
void PreferencePagePython::changeEvent(QEvent *e)
301
{
302
    QWidget::changeEvent(e);
303
}
304

305
void PreferencePagePython::loadSettings()
306
{
307
    Base::PyGILStateLocker lock;
308
    try {
309
        if (page.hasAttr(std::string("loadSettings"))) {
310
            Py::Callable method(page.getAttr(std::string("loadSettings")));
311
            Py::Tuple args;
312
            method.apply(args);
313
        }
314
    }
315
    catch (Py::Exception&) {
316
        Base::PyException e; // extract the Python error text
317
        e.ReportException();
318
    }
319
}
320

321
void PreferencePagePython::saveSettings()
322
{
323
    Base::PyGILStateLocker lock;
324
    try {
325
        if (page.hasAttr(std::string("saveSettings"))) {
326
            Py::Callable method(page.getAttr(std::string("saveSettings")));
327
            Py::Tuple args;
328
            method.apply(args);
329
        }
330
    }
331
    catch (Py::Exception&) {
332
        Base::PyException e; // extract the Python error text
333
        e.ReportException();
334
    }
335
}
336

337
// ----------------------------------------------------
338

339
/* TRANSLATOR Gui::ContainerDialog */
340

341
/**
342
 *  Constructs a ContainerDialog which embeds the child \a templChild.
343
 *  The dialog will be modal.
344
 */
345
ContainerDialog::ContainerDialog( QWidget* templChild )
346
  : QDialog( QApplication::activeWindow())
347
{
348
    setModal(true);
349
    setWindowTitle( templChild->objectName() );
350
    setObjectName( templChild->objectName() );
351

352
    setSizeGripEnabled( true );
353
    MyDialogLayout = new QGridLayout(this);
354

355
    buttonOk = new QPushButton(this);
356
    buttonOk->setObjectName(QLatin1String("buttonOK"));
357
    buttonOk->setText( tr( "&OK" ) );
358
    buttonOk->setAutoDefault( true );
359
    buttonOk->setDefault( true );
360

361
    MyDialogLayout->addWidget( buttonOk, 1, 0 );
362
    auto spacer = new QSpacerItem( 210, 20, QSizePolicy::Expanding, QSizePolicy::Minimum );
363
    MyDialogLayout->addItem( spacer, 1, 1 );
364

365
    buttonCancel = new QPushButton(this);
366
    buttonCancel->setObjectName(QLatin1String("buttonCancel"));
367
    buttonCancel->setText( tr( "&Cancel" ) );
368
    buttonCancel->setAutoDefault( true );
369

370
    MyDialogLayout->addWidget( buttonCancel, 1, 2 );
371

372
    templChild->setParent(this);
373

374
    MyDialogLayout->addWidget( templChild, 0, 0, 0, 2 );
375
    resize( QSize(307, 197).expandedTo(minimumSizeHint()) );
376

377
    // signals and slots connections
378
    connect( buttonOk, &QPushButton::clicked, this, &QDialog::accept);
379
    connect( buttonCancel, &QPushButton::clicked, this, &QDialog::reject);
380
}
381

382
/** Destroys the object and frees any allocated resources */
383
ContainerDialog::~ContainerDialog() = default;
384

385
// ----------------------------------------------------
386

387
void PyResource::init_type()
388
{
389
    behaviors().name("PyResource");
390
    behaviors().doc("PyResource");
391
    // you must have overwritten the virtual functions
392
    behaviors().supportRepr();
393
    behaviors().supportGetattr();
394
    behaviors().supportSetattr();
395
    add_varargs_method("value",&PyResource::value);
396
    add_varargs_method("setValue",&PyResource::setValue);
397
    add_varargs_method("show",&PyResource::show);
398
    add_varargs_method("connect",&PyResource::connect);
399
}
400

401
PyResource::PyResource() : myDlg(nullptr)
402
{
403
}
404

405
PyResource::~PyResource()
406
{
407
    delete myDlg;
408
    for (auto it : mySignals) {
409
        SignalConnect* sc = it;
410
        delete sc;
411
    }
412
}
413

414
/**
415
 * Loads an .ui file with the name \a name. If the .ui file cannot be found or the QWidgetFactory
416
 * cannot create an instance an exception is thrown. If the created resource does not inherit from
417
 * QDialog an instance of ContainerDialog is created to embed it.
418
 */
419
void PyResource::load(const char* name)
420
{
421
    QString fn = QString::fromUtf8(name);
422
    QFileInfo fi(fn);
423

424
    // checks whether it's a relative path
425
    if (fi.isRelative()) {
426
        QString cwd = QDir::currentPath ();
427
        QString home= QDir(QString::fromStdString(App::Application::getHomePath())).path();
428

429
        // search in cwd and home path for the file
430
        //
431
        // file does not reside in cwd, check home path now
432
        if (!fi.exists()) {
433
            if (cwd == home) {
434
                QString what = QObject::tr("Cannot find file %1").arg(fi.absoluteFilePath());
435
                throw Base::FileSystemError(what.toUtf8().constData());
436
            }
437
            else {
438
                fi.setFile( QDir(home), fn );
439

440
                if (!fi.exists()) {
441
                    QString what = QObject::tr("Cannot find file %1 neither in %2 nor in %3")
442
                        .arg(fn, cwd, home);
443
                    throw Base::FileSystemError(what.toUtf8().constData());
444
                }
445
                else {
446
                    fn = fi.absoluteFilePath(); // file resides in FreeCAD's home directory
447
                }
448
            }
449
        }
450
    }
451
    else {
452
        if (!fi.exists()) {
453
            QString what = QObject::tr("Cannot find file %1").arg(fn);
454
            throw Base::FileSystemError(what.toUtf8().constData());
455
        }
456
    }
457

458
    QWidget* w=nullptr;
459
    try {
460
        auto loader = UiLoader::newInstance();
461
        QFile file(fn);
462
        if (file.open(QFile::ReadOnly))
463
            w = loader->load(&file, QApplication::activeWindow());
464
        file.close();
465
    }
466
    catch (...) {
467
        throw Base::RuntimeError("Cannot create resource");
468
    }
469

470
    if (!w)
471
        throw Base::ValueError("Invalid widget.");
472

473
    if (w->inherits("QDialog")) {
474
        myDlg = static_cast<QDialog*>(w);
475
    }
476
    else {
477
        myDlg = new ContainerDialog(w);
478
    }
479
}
480

481
/**
482
 * Makes a connection between the sender widget \a sender and its signal \a signal
483
 * of the created resource and Python callback function \a cb.
484
 * If the sender widget does not exist or no resource has been loaded this method returns false,
485
 * otherwise it returns true.
486
 */
487
bool PyResource::connect(const char* sender, const char* signal, PyObject* cb)
488
{
489
    if ( !myDlg )
490
        return false;
491

492
    QObject* objS=nullptr;
493
    QList<QWidget*> list = myDlg->findChildren<QWidget*>();
494
    QList<QWidget*>::const_iterator it = list.cbegin();
495
    QObject *obj;
496
    QString sigStr = QString::fromLatin1("2%1").arg(QString::fromLatin1(signal));
497

498
    while ( it != list.cend() ) {
499
        obj = *it;
500
        ++it;
501
        if (obj->objectName() == QLatin1String(sender)) {
502
            objS = obj;
503
            break;
504
        }
505
    }
506

507
    if (objS) {
508
        auto sc = new SignalConnect(this, cb);
509
        mySignals.push_back(sc);
510
        return QObject::connect(objS, sigStr.toLatin1(), sc, SLOT ( onExecute() )  );
511
    }
512
    else
513
        qWarning( "'%s' does not exist.\n", sender );
514

515
    return false;
516
}
517

518
Py::Object PyResource::repr()
519
{
520
    std::string s;
521
    std::ostringstream s_out;
522
    s_out << "Resource object";
523
    return Py::String(s_out.str());
524
}
525

526
/**
527
 * Searches for a widget and its value in the argument object \a args
528
 * to returns its value as Python object.
529
 * In the case it fails nullptr is returned.
530
 */
531
Py::Object PyResource::value(const Py::Tuple& args)
532
{
533
    char *psName;
534
    char *psProperty;
535
    if (!PyArg_ParseTuple(args.ptr(), "ss", &psName, &psProperty))
536
        throw Py::Exception();
537

538
    QVariant v;
539
    if (myDlg) {
540
        QList<QWidget*> list = myDlg->findChildren<QWidget*>();
541
        QList<QWidget*>::const_iterator it = list.cbegin();
542
        QObject *obj;
543

544
        bool fnd = false;
545
        while ( it != list.cend() ) {
546
            obj = *it;
547
            ++it;
548
            if (obj->objectName() == QLatin1String(psName)) {
549
                fnd = true;
550
                v = obj->property(psProperty);
551
                break;
552
            }
553
        }
554

555
        if ( !fnd )
556
            qWarning( "'%s' not found.\n", psName );
557
    }
558

559
    Py::Object item = Py::None();
560
    switch (v.userType())
561
    {
562
    case QMetaType::QStringList:
563
        {
564
            QStringList str = v.toStringList();
565
            int nSize = str.count();
566
            Py::List slist(nSize);
567
            for (int i=0; i<nSize;++i) {
568
                slist.setItem(i, Py::String(str[i].toLatin1()));
569
            }
570
            item = slist;
571
        }   break;
572
    case QMetaType::QByteArray:
573
        break;
574
    case QMetaType::QString:
575
        item = Py::String(v.toString().toLatin1());
576
        break;
577
    case QMetaType::Double:
578
        item = Py::Float(v.toDouble());
579
        break;
580
    case QMetaType::Bool:
581
        item = Py::Boolean(v.toBool());
582
        break;
583
    case QMetaType::UInt:
584
        item = Py::Long(static_cast<unsigned long>(v.toUInt()));
585
        break;
586
    case QMetaType::Int:
587
        item = Py::Int(v.toInt());
588
        break;
589
    default:
590
        item = Py::String("");
591
        break;
592
    }
593

594
    return item;
595
}
596

597
/**
598
 * Searches for a widget, its value name and the new value in the argument object \a args
599
 * to set even this new value.
600
 * In the case it fails nullptr is returned.
601
 */
602
Py::Object PyResource::setValue(const Py::Tuple& args)
603
{
604
    char *psName;
605
    char *psProperty;
606
    PyObject *psValue;
607
    if (!PyArg_ParseTuple(args.ptr(), "ssO", &psName, &psProperty, &psValue))
608
        throw Py::Exception();
609

610
    QVariant v;
611
    if (PyUnicode_Check(psValue)) {
612
        v = QString::fromUtf8(PyUnicode_AsUTF8(psValue));
613

614
    }
615
    else if (PyLong_Check(psValue)) {
616
        unsigned int val = PyLong_AsLong(psValue);
617
        v = val;
618
    }
619
    else if (PyFloat_Check(psValue)) {
620
        v = PyFloat_AsDouble(psValue);
621
    }
622
    else if (PyList_Check(psValue)) {
623
        QStringList str;
624
        int nSize = PyList_Size(psValue);
625
        for (int i=0; i<nSize;++i) {
626
            PyObject* item = PyList_GetItem(psValue, i);
627
            if (!PyUnicode_Check(item))
628
                continue;
629
            const char* pItem = PyUnicode_AsUTF8(item);
630
            str.append(QString::fromUtf8(pItem));
631
        }
632

633
        v = str;
634
    }
635
    else {
636
        throw Py::TypeError("Unsupported type");
637
    }
638

639
    if (myDlg) {
640
        QList<QWidget*> list = myDlg->findChildren<QWidget*>();
641
        QList<QWidget*>::const_iterator it = list.cbegin();
642
        QObject *obj;
643

644
        bool fnd = false;
645
        while ( it != list.cend() ) {
646
            obj = *it;
647
            ++it;
648
            if (obj->objectName() == QLatin1String(psName)) {
649
                fnd = true;
650
                obj->setProperty(psProperty, v);
651
                break;
652
            }
653
        }
654

655
        if (!fnd)
656
            qWarning( "'%s' not found.\n", psName );
657
    }
658

659
    return Py::None();
660
}
661

662
/**
663
 * If any resource has been loaded this methods shows it as a modal dialog.
664
 */
665
Py::Object PyResource::show(const Py::Tuple&)
666
{
667
    if (myDlg) {
668
        // small trick to get focus
669
        myDlg->showMinimized();
670

671
#ifdef Q_WS_X11
672
        // On X11 this may not work. For further information see QWidget::showMaximized
673
        //
674
        // workaround for X11
675
        myDlg->hide();
676
        myDlg->show();
677
#endif
678

679
        myDlg->showNormal();
680
        myDlg->exec();
681
    }
682

683
    return Py::None();
684
}
685

686
/**
687
 * Searches for the sender, the signal and the callback function to connect with
688
 * in the argument object \a args. In the case it fails nullptr is returned.
689
 */
690
Py::Object PyResource::connect(const Py::Tuple& args)
691
{
692
    char *psSender;
693
    char *psSignal;
694

695
    PyObject *temp;
696

697
    if (PyArg_ParseTuple(args.ptr(), "ssO", &psSender, &psSignal, &temp)) {
698
        if (!PyCallable_Check(temp)) {
699
            PyErr_SetString(PyExc_TypeError, "parameter must be callable");
700
            throw Py::Exception();
701
        }
702

703
        Py_XINCREF(temp);         /* Add a reference to new callback */
704
        std::string sSender = psSender;
705
        std::string sSignal = psSignal;
706

707
        if (!connect(psSender, psSignal, temp)) {
708
            // no signal object found => dispose the callback object
709
            Py_XDECREF(temp);  /* Dispose of callback */
710
        }
711

712
        return Py::None();
713
    }
714

715
    // error set by PyArg_ParseTuple
716
    throw Py::Exception();
717
}
718

719
// ----------------------------------------------------
720

721
SignalConnect::SignalConnect(PyObject* res, PyObject* cb)
722
  : myResource(res), myCallback(cb)
723
{
724
}
725

726
SignalConnect::~SignalConnect()
727
{
728
    Base::PyGILStateLocker lock;
729
    Py_XDECREF(myCallback);  /* Dispose of callback */
730
}
731

732
/**
733
 * Calls the callback function of the connected Python object.
734
 */
735
void SignalConnect::onExecute()
736
{
737
    PyObject *arglist;
738
    PyObject *result;
739

740
    /* Time to call the callback */
741
    arglist = Py_BuildValue("(O)", myResource);
742
#if PY_VERSION_HEX < 0x03090000
743
    result = PyEval_CallObject(myCallback, arglist);
744
#else
745
    result = PyObject_CallObject(myCallback, arglist);
746
#endif
747
    Py_XDECREF(result);
748
    Py_DECREF(arglist);
749
}
750

751
#include "moc_WidgetFactory.cpp"
752

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.