-
Notifications
You must be signed in to change notification settings - Fork 0
/
Window.py
1184 lines (1041 loc) · 53.8 KB
/
Window.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# -*- coding: utf-8 -*-
import os
from typing import List, Tuple
from PyQt5.QtGui import QCursor, QIcon
from PyQt5.QtWidgets import QMenu, QFileDialog
from Dialogs import *
from LogicFunction import *
from ImageGenerator import graph
import json
import subprocess, platform
DEBUG_MODE = 0
def load_json(filename) -> dict:
"""! Загрузка JSON-файла сразу в словарь
@param filename: файл
@return: dict
"""
with open(filename) as file:
return json.load(file)
class SchemeCompilationError(Exception):
"""! Ошибка, вызываемая когда не удается скомпилировать схему.
"""
pass
class ProgramWindow(QMainWindow):
"""! Главное окно программы
"""
def __init__(self):
"""! Инициализация окна
"""
super().__init__()
uic.loadUi('resources/Window.ui', self)
self.canvas = MySchemeCanvas(self.schemeTab)
self.schemeTab.hide()
self.setWindowIcon(QIcon("resources/logo_new.png"))
# отключаем меню схемы (оно не нужно по умолчанию)
self.menuScheme.setEnabled(0)
# сама функция, с которой работаем
self.function = None
# привязка кнопок
# создание
self.buttonTypeManual.clicked.connect(self.create_function_manual)
self.buttonGenerateFromScheme.clicked.connect(self.open_scheme_editor)
self.buttonGenerateFromTable.clicked.connect(self.create_function_from_table)
# схема
self.action_scheme_compile.triggered.connect(self.prepare_compilation) # компиляция схемы
self.action_scheme_clear.triggered.connect(self.clear_canvas) # очистка схемы
self.action_scheme_open.triggered.connect(self.load_scheme) # загрузка схемы
self.action_scheme_save.triggered.connect(self.save_scheme) # сохранение схемы
self.action_scheme_quit.triggered.connect(self.close_scheme_editor) # закрытие схемы
# преобразования
self.button_convert.clicked.connect(self.run_conversion) # запуск преобразования
self.conversion_save.clicked.connect(self.save_conversion) # сохранение преобразования
self.conversion_clear.clicked.connect(self.clear_conversion_log) # очистка преобразования
# анализ
self.button_analyze_generate.clicked.connect(self.draw_graph) # предпросмотр
self.button_analyze_save.clicked.connect(self.save_graph) # сохранение
###
self.action_about.triggered.connect(self.view_about_page) # о программе
self.action_docs.triggered.connect(self.open_docs) # документация
# Горячие клавиши
self.action_scheme_open.setShortcut("Ctrl+O")
self.action_scheme_save.setShortcut("Ctrl+S")
self.action_scheme_compile.setShortcut("Ctrl+G")
self.action_scheme_clear.setShortcut("Ctrl+1")
self.action_scheme_quit.setShortcut("Ctrl+Q")
self.action_docs.setShortcut("F1")
self.setup_editor()
# АНАЛИЗАТОР
def save_graph(self):
"""! Сохранить график в файл PNG.
"""
# проверка, есть ли функция
if self.function is None:
dialog = WarnDialog("Ошибка", "Функция не задана")
dialog.exec_()
return
# создать диалоговое окно
# Использованные материалы: https://www.tutorialspoint.com/pyqt/pyqt_qfiledialog_widget.htm
dialog = QFileDialog(self)
dialog.setNameFilter("PNG (*.png)")
dialog.setAcceptMode(QFileDialog.AcceptSave)
if dialog.exec_():
filename = dialog.selectedFiles()[0]
# конец заимствования
# добавляем расширение, если оно не было указано
if '.png' not in filename:
filename += '.png'
self.draw_graph(filename)
def draw_graph(self, override_output=''):
"""! Отрисовать график-вейформу функции
@param override_output: куда сохранить файл. Если не указан, то файл сохранится как temp_graph.png
"""
if self.function is None:
dialog = WarnDialog("Ошибка", "Функция не задана")
dialog.exec_()
return
# цвета в соотвествии с выбором в выпадающих списках
bg_color = ['white', 'black'][self.analayze_color_selector_1.currentIndex()]
text_color = ['black', 'white'][self.analayze_color_selector_1.currentIndex()]
graph_color = ['blue', 'red', 'green', 'violet'][self.analayze_color_selector_2.currentIndex()]
# проверка на сохранение
if not override_output:
override_output = "resources/temp_graph.png"
graph(self.function, text_color, bg_color, graph_color, override_output)
pixmap = QPixmap(override_output)
pixmap = pixmap.scaledToHeight(self.graphScrollArea.height() - 24) # обрезаем по высоте окошка
self.analyze_graph.setPixmap(pixmap)
# ПРЕОБРАЗОВАНИЯ
def run_conversion(self):
"""! Запустить преобразование, выбранное в списке
"""
conv_type = self.conversion_selector.currentIndex()
if self.function is None:
# функция не задана
dialog = WarnDialog("Внимание", 'Сейчас исходная функция не задана.\n'
'Задайте ее на вкладке "запись".')
dialog.exec_()
return
if conv_type <= 4:
# просто другое отображение
converted = ("not ", " or ", " and ", " ^ ") # заменяемые символы
conversions = (("!", " | ", " & ", " ^ "),
("!", " + ", " * ", " == "),
("¬", " ⋁ ", " ⋀ ", " ⊕ "),
("not ", " or ", " and ", " xor "),
("не ", " или ", " и ", " ^ "))
exp = self.function.exp
for r_i in range(len(converted)):
exp = exp.replace(converted[r_i], conversions[conv_type][r_i])
self.conversion_log.setPlainText("F = " + exp)
elif conv_type <= 6:
# СДНФ/СКНФ
new_func = generate_function_from_table(self.function.generate_boolean_table(), 1 - conv_type % 2,
var_names=self.function.get_variables())
self.conversion_log.setPlainText("F = " + new_func.exp.replace(' ', ' '))
elif conv_type == 7:
# МКНФ
try:
new_func = self.function.simplify_sknf()
self.conversion_log.setPlainText("F = " + new_func.exp.replace(' ', ' '))
except InputException as error:
# функция равна константе
dialog = WarnDialog("Ошибка", error.args[0])
dialog.exec_()
except Exception:
dialog = WarnDialog("Ошибка", "Данный метод не способен упростить эту функцию, попробуйте другой.")
dialog.exec_()
elif conv_type == 8:
# МДНФ
try:
new_func = self.function.simplify_sdnf()
self.conversion_log.setPlainText("F = " + new_func.exp.replace(' ', ' '))
except InputException as error:
# функция равна константе
dialog = WarnDialog("Ошибка", error.args[0])
dialog.exec_()
except Exception:
dialog = WarnDialog("Ошибка", "Данный метод не способен упростить эту функцию, попробуйте другой.")
dialog.exec_()
def save_conversion(self):
"""! Сохранить преобразование как текущую функцию
"""
if self.conversion_log.toPlainText():
self.function = LogicFunction(self.conversion_log.toPlainText()[4:])
self.function_text.setText(self.conversion_log.toPlainText())
self.clear_conversion_log()
def clear_conversion_log(self):
"""! Очистить вывод преобразований
"""
self.conversion_log.setPlainText("")
# UTILITY
def view_about_page(self):
"""! Открыть окошко "О Программе"
"""
dialog = WarnDialog("О программе", "Анализатор логических функций TwinklingAnalyzer v1.0 \n\n"
"Баулин Филипп\nСеребрякова Ольга\nМИЭМ НИУ ВШЭ, 2022\n\n"
"https://github.com/Firyst/TwinklingAnalyzer")
dialog.exec_()
def open_docs(self):
"""! Открывает документацию в браузере.
"""
# код заимствован с https://stackoverflow.com/questions/434597/open-document-with-default-os-application-in-python-both-in-windows-and-mac-os
filepath = os.path.abspath('docs/html/index.html')
if platform.system() == 'Darwin': # macOS
subprocess.call(('open', filepath))
elif platform.system() == 'Windows': # Windows
os.startfile(filepath)
else: # linux variants
subprocess.call(('xdg-open', filepath))
# конец заимствования
def closeEvent(self, a0: QtGui.QCloseEvent) -> None:
"""! Событие закрытия окна программы. Используется для очистки кэша.
@param a0: событие от pyqt
"""
if os.path.exists("resources/temp_graph.png"):
os.remove("resources/temp_graph.png")
# ФУНКЦИИ ДЛЯ СХЕМЫ
def prepare_compilation(self):
"""! Запустить компиляцию и обработать результат
"""
try:
func = self.canvas.compile_scheme() # компиляция
if func is None:
return
dialog = ConfirmDialog(f"Результат компиляции",
f"Компиляция прошла успешно. Полученное выражение:\nF = {func.exp}\n"
f"Сохранить результат?")
dialog.exec_()
if dialog.output:
# сохраняем функцию
self.function = func
self.function_text.setText("F = " + func.exp)
self.close_scheme_editor()
except SchemeCompilationError as err:
dialog = WarnDialog("Ошибка компиляции", err.args[0])
dialog.exec_()
def clear_canvas(self):
"""! Очистить холст (удалить все виджеты)
"""
dialog = ConfirmDialog("Подтвердите действие", "Вы действительно хотите удалить все объекты?\n"
"Это действие необратимо.")
dialog.exec_()
if dialog.output == 1:
self.schemeTab.hide()
self.canvas.clear_all()
# добавить стандартный виджет выхода
obj = self.canvas.new_widget((DraggableWidget(self.schemeTab, 'output', self.canvas)))
obj.grid_pos = (1, 1)
self.schemeTab.show()
self.canvas.render_widgets()
def canvas_context_menu(self, event):
"""! Обработчик контекстного меню для холста
@param event: Событие от pyqt
"""
menu = QMenu(self.schemeTab)
add_action = menu.addAction("Новый элемент")
back_action = menu.addAction("Вернуться к началу координат")
action = menu.exec_(self.schemeTab.mapToGlobal(event.pos()))
if action == add_action:
# добавление нового элемента
dialog = LogicSelectDialog()
dialog.exec_()
if dialog.output is not None:
self.schemeTab.hide() # для правильной отрисовки нужно отключить события обновления
try:
widget = DraggableWidget(self.schemeTab, dialog.output, self.canvas)
except InputException:
self.schemeTab.show()
return
self.canvas.new_widget(widget)
widget.grid_pos = ((event.x()) // self.canvas.step + self.canvas.pos[0] // 20,
(event.y()) // self.canvas.step + self.canvas.pos[1] // 20)
widget.render_object()
self.schemeTab.show()
self.canvas.render_widgets()
elif action == back_action:
# вернуться к началу координат
self.canvas.pos = (0, 0)
self.canvas.render_widgets()
def close_scheme_editor(self):
"""! Выйти из редактора схем
"""
self.stackedWidget.setCurrentIndex(0)
self.menuScheme.setEnabled(0)
def open_scheme_editor(self):
"""! Открыть редактор схем
"""
self.stackedWidget.setCurrentIndex(1)
self.menuScheme.setEnabled(1)
# self.canvas.new_widget(DraggableWidget(self.schemeTab, 'resources/hecker.jpg', self.canvas))
self.canvas.render_widgets()
def setup_editor(self):
"""! Инициализировать окно редактора схем
"""
self.schemeTab.contextMenuEvent = self.canvas_context_menu
# добавить стандартный виджет выхода
obj = self.canvas.new_widget((DraggableWidget(self.schemeTab, 'output', self.canvas)))
obj.grid_pos = (1, 1)
self.schemeTab.show()
self.canvas.render_widgets()
def save_scheme(self):
"""! Сохранить схему в файл
"""
# создать диалоговое окно
# Использованные материалы: https://www.tutorialspoint.com/pyqt/pyqt_qfiledialog_widget.htm
dialog = QFileDialog(self)
dialog.setAcceptMode(QFileDialog.AcceptSave)
if dialog.exec_():
filename = dialog.selectedFiles()[0]
# конец заимствования
# добавляем расширение, если оно не было указано
if '.json' not in filename:
filename += '.json'
with open(filename, 'w') as file:
file.write(json.dumps(self.canvas.save_scheme(), indent=2))
def load_scheme(self):
"""! Загрузить схему из файла
"""
# создать диалоговое окно
# Использованные материалы: https://www.tutorialspoint.com/pyqt/pyqt_qfiledialog_widget.htm
dialog = QFileDialog(self)
dialog.setAcceptMode(QFileDialog.AcceptOpen)
dialog.setNameFilter("JSON (*.json)")
if dialog.exec_():
try:
data = load_json(dialog.selectedFiles()[0])
# конец заимствования
# проверка валидности JSON
if data['filetype'] == "TA-scheme-v1":
# загружаем схему
self.schemeTab.hide()
self.canvas.load_scheme(data)
self.schemeTab.show()
self.canvas.render_widgets()
else:
raise TypeError
except Exception:
dialog = WarnDialog("Ошибка", "Не удаётся прочитать файл.\n"
"Возможно он был поврежден или не является файлом схемы.")
dialog.exec_()
# create methods
def create_function_manual(self):
"""! Запись функции вручную (строкой)
"""
dialog = InputDialog()
dialog.exec_()
if dialog.output:
self.function = dialog.output
self.function_text.setText("F = " + dialog.inputFunction.text())
def create_function_from_table(self):
"""! Запись функции с помощью таблицы истинности
"""
dialog = TableDialog()
dialog.exec_()
if dialog.output:
self.function = dialog.output
self.function_text.setText("F = " + dialog.output.exp)
class DraggableWidget(QLabel):
"""! Перетаскиваемый виджет. Используется как логический элемент
"""
def __init__(self, parent: QWidget, object_type: str, canvas, override_name=''):
"""! Создать виджет объекта схемы
@param parent: родительских виджет.
@param object_type: тип картинки. Возможные значения: and, or, not, inp, out, xor, debug
@param canvas: холст, на котором располагается виджет
@param override_name: название входной переменной. Используется только для типа input. Необязательный аргумент
-- по умолчанию будет вызываться диалоговое окно.
"""
super().__init__(parent)
self.canvas = canvas
self.connectors: List[Connector] = []
self.properties = load_json(os.path.join('resources', 'elements', f'{object_type}.json'))
# переменные для расчета перетаскиваний
self.cdx, self.cdy = 0, 0 # стартовая позиция курсора
self.dragging = False # перетягивается ли объект сейчас
self.grid_pos = (0, 0) # позиция на сетке
self.setScaledContents(True) # растягивание картинки
self.obj_type = object_type
self.setPixmap(QPixmap(self.properties['image']))
if object_type == 'input':
# входной сигнал
if override_name:
# имя переменной заранее задано, поэтому диалог не нужен
res = override_name
else:
# если просто создаем объект, то вызываем диалог
dialog = TinyInputDialog()
dialog.exec_()
res = dialog.output
if dialog.output is None:
self.deleteLater()
raise InputException("Не задано имя переменной")
# создание подписи
self.name_widget = QLabel(self)
self.name_widget.setText(res)
self.name_widget.setAlignment(Qt.AlignBottom)
current_font = self.name_widget.font()
current_font.setPointSize(12)
self.name_widget.setFont(current_font)
self.properties['name'] = res
self.add_connectors()
def add_connectors(self):
"""! Добавить коннекторы к элементу. Выполняется при инициализации
"""
for connector in self.properties['connectors']:
new_connector = Connector(self, self.properties['connectors'][connector], connector)
self.connectors.append(new_connector)
self.canvas.connectors.append(new_connector)
def contextMenuEvent(self, event):
"""! Контекстное меню для элемента.
@param event: событие от pyqt
"""
if self.obj_type == "output":
return # с выходным сигналом ничего сделать нельзя
menu = QMenu(self)
clone_action = menu.addAction("Дублировать")
delete_action = menu.addAction("Удалить")
action = menu.exec_(self.mapToGlobal(event.pos()))
if action == clone_action:
# дублировать объект
self.canvas.parent.hide() # для правильной отрисовки нужно отключить события обновления
try:
widget = self.canvas.new_widget(DraggableWidget(self.canvas.parent, self.obj_type, self.canvas))
except InputException:
# если дублировали input и не стали задавать имя
self.canvas.parent.show()
return
widget.grid_pos = (self.grid_pos[0] + 1, self.grid_pos[1] + 1) # задать смещенную позицию
self.canvas.parent.show()
self.canvas.render_widgets()
elif action == delete_action:
# удалить объект
self.deleteLater()
for con in self.connectors:
# удалить все коннекторы
self.canvas.connectors.remove(con)
con.deleteLater()
self.canvas.widgets.remove(self)
self.canvas.render_widgets()
def render_object(self):
"""! Отрисовывает объект на схеме.
"""
self.set_pos(-self.canvas.pos[0] * self.canvas.zoom + self.grid_pos[0] * self.canvas.step,
-self.canvas.pos[1] * self.canvas.zoom + self.grid_pos[1] * self.canvas.step)
self.set_size(self.properties['size'][0] * self.canvas.step,
self.properties['size'][1] * self.canvas.step)
def set_pos(self, x: int, y: int):
"""! Устанавливает положение объекта на экране
@param x: координата X
@param y: координата Y
"""
self.setGeometry(x, y, self.geometry().width(), self.geometry().height())
self.canvas.compile_connectors()
def set_size(self, w: int, h: int):
"""! Устанавливает размер объекта
@param w: ширина
@param h: высота
"""
self.setGeometry(self.geometry().x(), self.geometry().y(), w, h)
self.pixmap().scaled(w, h, Qt.IgnoreAspectRatio)
for connector in self.connectors:
connector.render_size()
def get_grid_pos(self) -> Tuple[int, int]:
"""! Получить координаты объекта на сетке. Метод нужен для унификации с другими объектами.
@return: Tuple(int, int) - позиция на сетке.
"""
return self.grid_pos
def get_size(self) -> Tuple[int, int]:
"""! Получить текущий размер объекта кортежем
@return:Tuple(int, int) - отрисованный размер объекта.
"""
return (self.geometry().width(), self.geometry().height())
def mousePressEvent(self, ev: QtGui.QMouseEvent) -> None:
"""! Событие нажатия мыши
@param ev: событие от pyqt
"""
if ev.button() == Qt.LeftButton:
# ЛКМ
self.dragging = 1
self.cdx, self.cdy = self.x() - QCursor.pos().x(), self.y() - QCursor.pos().y()
QApplication.setOverrideCursor(Qt.ClosedHandCursor) # изменить курсор
def mouseReleaseEvent(self, ev: QtGui.QMouseEvent) -> None:
"""! Событие отпуска кнопки мыши
@param ev: событие от pyqt
"""
self.dragging = 0
QApplication.restoreOverrideCursor() # вернуть курсор
def mouseMoveEvent(self, ev: QtGui.QMouseEvent) -> None:
"""! Событие перемещения мыши
@param ev: событие от pyqt
"""
if self.dragging:
# перетаскивание объекта мышкой
self.grid_pos = ((QCursor.pos().x() + self.cdx + self.canvas.pos[0] * self.canvas.zoom) // self.canvas.step,
(QCursor.pos().y() + self.cdy + self.canvas.pos[1] * self.canvas.zoom) // self.canvas.step)
self.render_object()
def __repr__(self) -> str:
"""! Запись данных объекта в строчку
@return: строка lObject(data...)
"""
return f"lObject({self.get_grid_pos()}, {self.obj_type})"
class Connector(QLabel):
"""! Класс пина для соединения элементов
"""
def __init__(self, parent, offset: tuple, usage: str):
"""! Инициализировать коннектор
@param parent: родитель, DraggableWidget/Connection
@param offset: смещение по сетке относительно родителя Tuple(int, int)
@param usage: тип коннектора input/output/bypass
"""
self.offset = offset
super().__init__(parent)
self.parent = parent
self.usage = usage
self.setScaledContents(True)
self.setPixmap(QPixmap("resources/connector_missing.png"))
self.test_line = None # тестовая линия для отрисовки соединений в realtime
self.render_size()
def set_type(self, new_type):
"""! Установить тип соединения
@param new_type: тип (missing, normal, intersection)
"""
if new_type == "missing":
self.setPixmap(QPixmap("resources/connector_missing.png"))
elif new_type == "normal":
self.setPixmap(QPixmap("resources/connector_default.png"))
elif new_type == 'intersection':
self.setPixmap(QPixmap("resources/connector_normal.png"))
def render_size(self):
"""! Отрисовать коннектор с текущими параметрами
"""
step = self.parent.canvas.step # grid step
self.setGeometry(self.offset[0] * step, self.offset[1] * step, step, step)
def get_grid_pos(self) -> Tuple[int, int]:
"""! Получить координаты объекта на сетке.
@return: Tuple(int, int) - позиция на сетке.
"""
return (self.parent.get_grid_pos()[0] + self.offset[0], self.parent.get_grid_pos()[1] + self.offset[1])
def set_size(self, w: int, h: int):
"""! Устанавливает размер объекта
@param w: ширина
@param h: высота
"""
self.setGeometry(self.geometry().x(), self.geometry().y(), w, h)
self.pixmap().scaled(w, h, Qt.KeepAspectRatio)
def mousePressEvent(self, ev: QtGui.QMouseEvent) -> None:
"""! Событие нажатия мыши
@param ev: событие от pyqt
"""
QApplication.setOverrideCursor(Qt.ClosedHandCursor)
def mouseReleaseEvent(self, ev: QtGui.QMouseEvent) -> None:
"""! Событие отпуска кнопки мыши
@param ev: событие от pyqt
"""
QApplication.restoreOverrideCursor()
def mouseMoveEvent(self, ev: QtGui.QMouseEvent) -> None:
"""! Событие перемещения мыши
@param ev: событие от pyqt
"""
current_grid_pos = (self.parent.get_grid_pos()[0] + self.offset[0],
self.parent.get_grid_pos()[1] + self.offset[1])
canvas_pos = self.parent.canvas.parent.mapFromGlobal(self.mapToGlobal(ev.pos())) # к-ты на холсте
mouse_grid_pos = (round((canvas_pos.x() ) / self.parent.canvas.step + self.parent.canvas.pos[0] / 20 - 0.5),
round((canvas_pos.y() ) / self.parent.canvas.step + self.parent.canvas.pos[1] / 20 - 0.5))
if abs(mouse_grid_pos[0] - current_grid_pos[0]) + abs(mouse_grid_pos[1] - current_grid_pos[1]) > 0:
# Курсор отодвинулся
self.parent.canvas.parent.hide()
if self.test_line is not None:
# удаляем старую пробную линию, если она была
self.test_line.delete()
# создаем пробную линию для отрисовки соединения
self.test_line = Connection2(self.parent.canvas, current_grid_pos, mouse_grid_pos)
self.parent.canvas.render_widgets()
self.parent.canvas.parent.show()
else:
# курсор не двигался
if self.test_line is not None:
self.test_line.delete()
self.test_line = None
def __repr__(self):
"""! Запись данных объекта в строчку
@return: строка Connector(data...)
"""
return f"Connector({self.get_grid_pos()}, {self.usage})"
class Connection(QLabel):
"""! Класс линии для соединения пинов
"""
def __init__(self, parent, p1: tuple, p2: tuple, orientation=0):
"""! Инициализровать линию и отрисовать ее. Координаты будут обрезаться по первой точке.
@param p1: первая точка
@param p2: вторая точка
@param orientation: ориентация линии: 0=горизонтальная, 1=вертикальная
"""
super().__init__(parent.parent)
self.canvas = parent # холст, на котором находится линия
self.orientation = orientation
self.pos = (p1, p2)
# создать и добавить два коннектора
self.connector1 = Connector(self, (0, 0), 'bypass')
self.connector2 = Connector(self, (0, 0), 'bypass')
self.canvas.connectors.append(self.connector1)
self.canvas.connectors.append(self.connector2)
self.line = QLabel(self) # видимая линия (смещена на половину шага сетки, чтобы быть по центру)
self.line.setScaledContents(True)
# выбор картинки в зависимости от ориентации
if orientation:
self.line.setPixmap(QPixmap("resources/lineV.png"))
else:
self.line.setPixmap(QPixmap("resources/lineH.png"))
self.render_line()
def contextMenuEvent(self, event):
"""! Событие контекстного меню для линии
@param event: событие от pyqt
"""
menu = QMenu(self)
delete_action = menu.addAction("Удалить соединение")
action = menu.exec_(self.mapToGlobal(event.pos()))
if action == delete_action:
# удалить объект
self.delete()
def get_grid_pos(self) -> Tuple[int, int]:
"""! Возвращает позицию на сетке начала линии
@return: Tuple[int, int] начала линии
"""
return self.pos[0]
def delete(self):
"""! Удалить объект и ссылки на него в объекте холста. Также удаляет и коннекторы.
"""
self.canvas.connectors.remove(self.connector1)
self.canvas.connectors.remove(self.connector2)
self.connector1.deleteLater()
self.connector2.deleteLater()
self.deleteLater()
# если не была удалена до этого
try:
self.canvas.lines.remove(self)
except ValueError:
pass
self.canvas.compile_connectors()
def render_line(self):
"""! Отрисовать линию на холсте
"""
if self.orientation == 0 and self.pos[1][0] < self.pos[0][0]:
self.pos = (self.pos[1], self.pos[0])
if self.orientation == 1 and self.pos[1][1] < self.pos[0][1]:
self.pos = (self.pos[1], self.pos[0])
self.set_pos(-self.canvas.pos[0] * self.canvas.zoom + self.pos[0][0] * self.canvas.step,
-self.canvas.pos[1] * self.canvas.zoom + self.pos[0][1] * self.canvas.step)
if self.orientation:
# вертикальная линия
self.set_size(self.canvas.step, self.canvas.step * (self.pos[1][1] - self.pos[0][1] + 1))
self.connector2.offset = (0, self.pos[1][1] - self.pos[0][1])
self.line.setGeometry(int(0.5 * (1 - self.orientation) * self.canvas.step),
int(0.5 * self.orientation * self.canvas.step),
self.canvas.step, self.canvas.step * (self.pos[1][1] - self.pos[0][1]))
else:
# горизональная линия
self.set_size(self.canvas.step * (self.pos[1][0] - self.pos[0][0] + 1), self.canvas.step)
self.connector2.offset = (self.pos[1][0] - self.pos[0][0], 0)
self.line.setGeometry(int(0.5 * (1 - self.orientation) * self.canvas.step),
int(0.5 * self.orientation * self.canvas.step),
self.canvas.step * (self.pos[1][0] - self.pos[0][0]), self.canvas.step)
self.connector1.render_size()
self.connector2.render_size()
def set_pos(self, x: int, y: int):
"""! Устанавливает положение левой/верхней точки на экране
@param x: координата X
@param y: координата Y
"""
self.setGeometry(x, y, self.geometry().width(), self.geometry().height())
def set_size(self, w: int, h: int):
"""! Устанавливает размер соединения (в шагах сетки)
@param w: ширина
@param h: высота
"""
self.setGeometry(self.geometry().x(), self.geometry().y(), w, h)
self.line.pixmap().scaled(self.line.geometry().width(), self.line.geometry().height(), Qt.IgnoreAspectRatio)
def __repr__(self):
"""! Запись данных объекта в строчку
@return: строка Line(data...)
"""
return f"Line{self.pos}"
class Connection2:
"""! Класс, опсиывающий двойную линию (вертикальная + горизонтальная), которой можно соеденить две
любый точке на холсте
"""
def __init__(self, parent, p1, p2):
"""! Инициализровать двойное соединение
@param parent: родитель (холст)
@param p1: Первая точка
@param p2: Вторая точка
"""
self.parent = parent
self.p1 = p1
self.p2 = p2
# создать объектьы линий
self.line_h = Connection(parent, (p1[0], p2[1]), (p2[0], p2[1]), 0)
self.line_v = Connection(parent, (p1[0], p1[1]), (p1[0], p2[1]), 1)
# добавить их в список в родителе-холсте
self.parent.new_line(self.line_v)
self.parent.new_line(self.line_h)
self.set_pos(self.p1, self.p2)
def delete(self):
"""! Удалить объект и его детей
"""
try:
self.line_v.delete()
except AttributeError:
pass
except ValueError:
pass
try:
self.line_h.delete()
except AttributeError:
pass
except ValueError:
pass
def render_connection(self):
"""! Отрисовать оба соединения
"""
if self.line_v is not None:
self.line_v.render_line()
if self.line_h is not None:
self.line_h.render_line()
def set_pos(self, p1: Tuple[int, int], p2: Tuple[int, int]):
"""! Установить начальную и конечную точки.
@param p1: первая точка
@param p2: вторая точка
"""
self.p1 = p1
self.p2 = p2
self.line_h.pos = ((p1[0], p2[1]), (p2[0], p2[1]))
self.line_v.pos = ((p1[0], p1[1]), (p1[0], p2[1]))
if (p1[0], p2[1]) == (p2[0], p2[1]):
self.line_h.delete()
self.line_h = None
if (p1[0], p1[1]) == (p1[0], p2[1]):
self.line_v.delete()
self.line_v = None
self.render_connection()
class MySchemeCanvas:
"""! Холст для создания схем
"""
def __init__(self, parent: QWidget):
"""! Инициализировать холст
@param parent: родитель - виджет, на который будут добавляться объекты
"""
self.parent: QWidget = parent
self.widgets: List[DraggableWidget] = list()
self.lines: List[Connection] = list()
self.connectors: List[Connector] = list()
self.zoom = 1 # к-т приближения
self.step = self.zoom * 20 # шаг сетки
self.pos = (0, 0) # позиция камеры (то есть левого верхнего угла холста)
# переменные для расчета перетаскиваний
self.cdx, self.cdy = 0, 0 # стартовая позиция курсора
self.dragging = False # перетягивается ли объект сейчас
# перенаправление событий из виджета
self.parent.mousePressEvent = self.mouse_press
self.parent.mouseReleaseEvent = self.mouse_release
self.parent.mouseMoveEvent = self.mouse_move
self.parent.wheelEvent = self.scroll
self.render_widgets()
def clear_all(self):
"""! Удалить все объекты на поле"""
for con in self.connectors:
con.parent.deleteLater()
con.deleteLater()
self.widgets = []
self.lines = []
self.connectors = []
def save_scheme(self) -> dict:
"""! Преобразовать схему в JSON-словарь
@return: dict со всеми элементами
"""
# установим специальную метку, чтобы определять что файл валидный
output_data = {'filetype': 'TA-scheme-v1', 'objects': []}
added = set() # учет всех добавленных
for con in self.connectors:
if con.parent not in added:
# проверяем родителя коннектора
obj = {}
if type(con.parent) == DraggableWidget:
# логический элемент
obj['type'] = 'Element'
obj['obj_type'] = con.parent.obj_type
obj['pos'] = list(con.parent.get_grid_pos())
if 'name' in con.parent.properties:
obj['name'] = con.parent.properties['name']
elif type(con.parent) == Connection:
# линия
obj['type'] = 'Line'
obj['pos1'] = con.parent.pos[0]
obj['pos2'] = con.parent.pos[1]
obj['orientation'] = con.parent.orientation
if obj:
output_data['objects'].append(obj)
added.add(con.parent)
if con not in added:
# проверяем сам коннектор
# а зачем..
pass
return output_data
def load_scheme(self, objects: dict):
"""! Загружает схему из JSON-словаря
@param objects: результат json.load()
"""
self.clear_all()
for obj in objects['objects']:
if obj['type'] == 'Element':
if 'name' in obj:
# это input (у него есть особенный параметр - имя)
new = self.new_widget(DraggableWidget(self.parent, obj['obj_type'], self, obj['name']))
else:
# это всё остальное
new = self.new_widget(DraggableWidget(self.parent, obj['obj_type'], self))
new.grid_pos = tuple(obj['pos'])
elif obj['type'] == 'Line':
self.new_line(Connection(self, tuple(obj['pos1']), tuple(obj['pos2']), obj['orientation']))
def new_widget(self, widget: DraggableWidget):
"""! Добавить виджет на холст
@param widget: Виджет класса DraggableWidget
"""
self.widgets.append(widget)
return widget
def new_line(self, widget: Connection):
"""! Добавить виджет на холст
@param widget: Виджет класса Connection
"""
self.lines.append(widget)
return widget
def render_widgets(self):
"""! Отрисовывает все виджеты
"""
for widget in self.widgets:
widget.render_object()
for line in self.lines:
line.render_line()
def compile_connectors(self) -> dict:
"""! Провести расчет всех соединений (коннектор) на холсте
@return сгрупированные по позиции коннекторы
"""
# будем группировать по позиции на холсте
grouped = dict()
for connector in self.connectors:
pos = connector.get_grid_pos()
if pos not in grouped:
grouped[pos] = [connector]
else:
grouped[pos].append(connector)
for pos in grouped:
if len(grouped[pos]) > 1:
for connector in grouped[pos]:
if len(grouped[pos]) > 2:
connector.set_type("intersection")
else:
connector.set_type("normal")
elif len(grouped[pos]) == 1:
grouped[pos][0].set_type("missing")
return grouped
def compile_scheme(self):
"""! Преобразует схему в выражение
@return: LogicFunction от схемы
"""
def debug_print(a):
# мини-функция для отладки
if DEBUG_MODE:
print(a)
def get_source(cur_w, vis=None):
"""! Ищет первый подключенный выход к сети соединений
@param cur_w: начальный объект
@param vis: список посещенных
@return: найденный коннектор
"""
if vis is None:
vis = list()
vis.append(cur_w)
if type(cur_w) == Connector:
if cur_w.usage == 'out':
return cur_w
elif cur_w.usage == 'bypass':
# проходной коннектор
connected = grouped[cur_w.get_grid_pos()].copy()
for con in connected:
if con not in vis:
# смотрим, есть ли искомый коннектор в данной ветке
if con.usage == 'out':
return cur_w
if con.usage[2:] == 'in':
return
vis.append(con)