Compare commits
No commits in common. "working-on-gui" and "master" have entirely different histories.
working-on
...
master
|
|
@ -1,2 +0,0 @@
|
||||||
/.venv
|
|
||||||
/.idea
|
|
||||||
210
abacus_core.py
210
abacus_core.py
|
|
@ -8,169 +8,91 @@ class AbacusCore:
|
||||||
self._vars = dict()
|
self._vars = dict()
|
||||||
self._funcs = dict()
|
self._funcs = dict()
|
||||||
|
|
||||||
self._vars['result'] = 0
|
|
||||||
self._vars['alphabet'] = 28
|
self._vars['alphabet'] = 28
|
||||||
self._vars['alpha'] = 1
|
self._vars['alpha'] = 1
|
||||||
self._vars['beta'] = 2
|
self._vars['beta'] = 2
|
||||||
|
|
||||||
self._operators = ['+', '-', '/', '*']
|
self._funcs['r2'] = lambda x: round(x, 2)
|
||||||
self._delimiters = ['(', ')', '[', ']', ';', ' ']
|
self._funcs['r0'] = lambda x: int(x)
|
||||||
|
|
||||||
def get_vars(self):
|
|
||||||
return self._vars
|
|
||||||
|
|
||||||
def get_funcs(self):
|
|
||||||
return self._funcs
|
|
||||||
|
|
||||||
def add_var(self, name, value):
|
|
||||||
self._vars[name] = value
|
|
||||||
|
|
||||||
def _calculate(self, input_str):
|
def _calculate(self, input_str):
|
||||||
print('input:', input_str)
|
|
||||||
try:
|
try:
|
||||||
return eval(input_str)
|
return True, eval(input_str)
|
||||||
except ZeroDivisionError:
|
except ZeroDivisionError:
|
||||||
res = 'Division by Zero'
|
res = 'Division by Zero'
|
||||||
except NameError as e:
|
except NameError as e:
|
||||||
res = 'Variable '+ e.name + ' not exists'
|
res = 'Variable '+ e.name + ' not exists'
|
||||||
except SyntaxError as e:
|
return False, res
|
||||||
res = 'Syntax Error'
|
|
||||||
return res
|
def get_vars(self):
|
||||||
|
return self._vars
|
||||||
|
|
||||||
|
def add_var(self, name, value):
|
||||||
|
self._vars[name] = value
|
||||||
|
|
||||||
def parse_input(self, input_str):
|
def parse_input(self, input_str):
|
||||||
'''
|
|
||||||
versuche input_str zu parsen in comment vars und andere teile
|
|
||||||
:param input_str:
|
|
||||||
:return:
|
|
||||||
'''
|
|
||||||
comment = None
|
comment = None
|
||||||
orig_input_str = input_str
|
input_wo_com = None
|
||||||
if len(input_str) < 1: # leere eingabe wird nicht akzeptiert
|
success = False
|
||||||
return
|
input_str = str(input_str).strip()
|
||||||
chunks = input_str.split('#')
|
|
||||||
if len(chunks) > 1:
|
|
||||||
input_str = chunks[0]
|
|
||||||
comment = ' '.join(chunks[1:])
|
|
||||||
chunks = input_str.split('=') # gibt es variablenzuweisung?
|
|
||||||
if len(chunks) > 2:
|
|
||||||
raise ValueError('Mehrfachzuweisung wird nicht unterstützt')
|
|
||||||
elif len(chunks) > 1:
|
|
||||||
expression = self._with_variable_another_way(chunks[1])
|
|
||||||
self._vars[chunks[0]] = expression
|
|
||||||
return
|
|
||||||
chunks = input_str.split(':') # gibt es funktionsdefinition?
|
|
||||||
if len(chunks) > 1:
|
|
||||||
self._funcs[chunks[0]] = chunks[1]
|
|
||||||
return
|
|
||||||
res = self._with_variable_another_way(input_str)
|
|
||||||
self._vars['result'] = res
|
|
||||||
return orig_input_str, None, res, comment
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def _with_variable_another_way(self, input_str):
|
|
||||||
input_str, comment = self._get_input_wo_commentar(input_str)
|
|
||||||
input_str = ''.join(input_str.split()) # entferne alle leerzeichen
|
|
||||||
input_str = input_str.replace(',', '.')
|
input_str = input_str.replace(',', '.')
|
||||||
opdel = self._operators + self._delimiters
|
if input_str[0] == '#':
|
||||||
chunks = list()
|
comment = input_str[1:]
|
||||||
last_index = 0
|
com_index = input_str.find('#')
|
||||||
for i, v in enumerate(input_str):
|
if com_index > -1:
|
||||||
if v in opdel:
|
input_wo_com = input_str[:com_index]
|
||||||
chunk = input_str[last_index:i]
|
comment = input_str[com_index + 1:]
|
||||||
chunk = self._is_a_variable_or_func(chunk)
|
if input_wo_com is None:
|
||||||
chunks.append(chunk)
|
input_wo_com = input_str
|
||||||
chunks.append(input_str[i])
|
print('comment', comment)
|
||||||
last_index = i+1
|
result = self._split_input(input_wo_com)
|
||||||
elif i == len(input_str)-1:
|
print('result:',result)
|
||||||
chunk = input_str[last_index:i+1]
|
if result[1] is not None:
|
||||||
chunk = self._is_a_variable_or_func(chunk)
|
success, res = self._calculate(result[1])
|
||||||
chunks.append(chunk)
|
if success:
|
||||||
return eval(''.join(chunks))
|
operators = result[0]
|
||||||
|
operators.reverse()
|
||||||
def _is_a_variable_or_func(self, chunk):
|
for op in operators:
|
||||||
var = self._vars.get(chunk)
|
if op[-1] == '=':
|
||||||
if not var:
|
self._vars[op[:-1]] = res
|
||||||
var = self._funcs.get(chunk)
|
elif op[-1] == ':':
|
||||||
if var:
|
res = self._funcs.get(op[:-1])(res)
|
||||||
var = self._with_variable_another_way(var)
|
print('vars:', self._vars)
|
||||||
if var:
|
|
||||||
return str(var)
|
|
||||||
else:
|
else:
|
||||||
return chunk
|
print("Error")
|
||||||
|
|
||||||
|
|
||||||
|
def _split_input(self, input_str):
|
||||||
|
last_position = 0
|
||||||
def _replace_vars(self, input_str):
|
varchunks = list()
|
||||||
'''
|
|
||||||
try to find vars and replace them
|
|
||||||
:param input_str:
|
|
||||||
:return:
|
|
||||||
'''
|
|
||||||
first_position = None
|
|
||||||
new_input_str = ''
|
|
||||||
op_del = self._operators + self._delimiters
|
|
||||||
if input_str[0] in self._operators:
|
|
||||||
input_str = 'result' + input_str
|
|
||||||
input_str_len = len(input_str)
|
|
||||||
for i, c in enumerate(input_str):
|
for i, c in enumerate(input_str):
|
||||||
if c in op_del or i == input_str_len-1: # and first_position is not None:
|
if c in [':', '=']:
|
||||||
if first_position is not None:
|
chunk = input_str[last_position:i].strip()
|
||||||
last_position = i
|
chunk = chunk + input_str[i]
|
||||||
if i == input_str_len-1:
|
varchunks.append(chunk)
|
||||||
last_position += 1
|
last_position = i+1
|
||||||
temp_var = input_str[first_position:last_position]
|
rest = input_str[last_position:].strip()
|
||||||
print('var', temp_var)
|
print('rest', rest)
|
||||||
var = self._vars.get(temp_var)
|
rest = self._find_vars(rest)[1]
|
||||||
if var is not None:
|
return varchunks, rest
|
||||||
new_input_str += str(var)
|
|
||||||
else:
|
|
||||||
func = self._funcs.get(temp_var)
|
|
||||||
if func:
|
|
||||||
new_input_str += str(func)
|
|
||||||
else:
|
|
||||||
raise NotImplemented()
|
|
||||||
first_position = None
|
|
||||||
if i == input_str_len - 1:
|
|
||||||
continue
|
|
||||||
new_input_str += input_str[i]
|
|
||||||
elif c.isalpha() or c=='_':
|
|
||||||
if first_position is None:
|
|
||||||
first_position = i
|
|
||||||
else:
|
|
||||||
new_input_str += input_str[i]
|
|
||||||
return new_input_str
|
|
||||||
|
|
||||||
def _get_input_wo_commentar(self, input_str):
|
def _find_vars(self, input_str):
|
||||||
'''
|
chunks = list()
|
||||||
Zerlege input in input und kommentar
|
last_position = 0
|
||||||
:param input_str:
|
for i, c in enumerate(input_str):
|
||||||
:return:
|
if c in ['+', '-', '/', '*', '(', ')', '[', ']', ';']:
|
||||||
'''
|
ch = input_str[last_position:i].strip()
|
||||||
input_str = input_str.strip()
|
if len(ch)>0:
|
||||||
comment_index = input_str.find('#')
|
chunks.append(ch)
|
||||||
if comment_index == 0: # die ganze zeile ist ein kommentar
|
chunks.append(input_str[i].strip())
|
||||||
return None, input_str
|
last_position = i+1
|
||||||
elif comment_index == -1:
|
|
||||||
return input_str, None
|
|
||||||
else:
|
|
||||||
comment = input_str[comment_index + 1:]
|
|
||||||
input_str = input_str[:comment_index]
|
|
||||||
return input_str, comment
|
|
||||||
|
|
||||||
|
if last_position<len(input_str):
|
||||||
def _is_new_variable(self, input_str):
|
chunks.append(input_str[last_position:].strip())
|
||||||
'''
|
cp_chunks = chunks.copy()
|
||||||
|
for i, chunk in enumerate(chunks):
|
||||||
'''
|
if chunk in self._vars:
|
||||||
chunks = input_str.split('=')
|
cp_chunks[i] = str(self._vars.get(chunk))
|
||||||
chunks_len = len(chunks)
|
print(chunks, cp_chunks)
|
||||||
if chunks_len > 2:
|
return len(cp_chunks)>1, ''.join(cp_chunks)
|
||||||
raise ValueError()
|
|
||||||
# chunks.reverse()
|
|
||||||
elif chunks_len == 2:
|
|
||||||
return chunks[1], chunks[0]
|
|
||||||
else:
|
|
||||||
return chunks[0], None
|
|
||||||
|
|
|
||||||
761
abacus_ide.py
761
abacus_ide.py
|
|
@ -1,761 +0,0 @@
|
||||||
import math
|
|
||||||
import sys
|
|
||||||
|
|
||||||
from PySide6.QtCore import Qt, QRect, QSize
|
|
||||||
from PySide6.QtGui import (
|
|
||||||
QColor,
|
|
||||||
QFont,
|
|
||||||
QPainter,
|
|
||||||
QSyntaxHighlighter,
|
|
||||||
QTextCharFormat,
|
|
||||||
)
|
|
||||||
from PySide6.QtWidgets import (
|
|
||||||
QApplication,
|
|
||||||
QDockWidget,
|
|
||||||
QFrame,
|
|
||||||
QHBoxLayout,
|
|
||||||
QLabel,
|
|
||||||
QLineEdit,
|
|
||||||
QListWidget,
|
|
||||||
QMainWindow,
|
|
||||||
QPlainTextEdit,
|
|
||||||
QPushButton,
|
|
||||||
QSplitter,
|
|
||||||
QStatusBar,
|
|
||||||
QTabWidget,
|
|
||||||
QTableWidget,
|
|
||||||
QTableWidgetItem,
|
|
||||||
QToolBar,
|
|
||||||
QVBoxLayout,
|
|
||||||
QWidget,
|
|
||||||
)
|
|
||||||
|
|
||||||
from abacus_core import AbacusCore
|
|
||||||
|
|
||||||
|
|
||||||
STYLE = """
|
|
||||||
QMainWindow, QWidget {
|
|
||||||
background: #0d1117;
|
|
||||||
color: #e6edf3;
|
|
||||||
font-family: "Segoe UI";
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
QToolBar {
|
|
||||||
background: #111820;
|
|
||||||
border: 0;
|
|
||||||
border-bottom: 1px solid #202a36;
|
|
||||||
spacing: 5px;
|
|
||||||
padding: 6px 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
QToolButton {
|
|
||||||
color: #c9d1d9;
|
|
||||||
background: transparent;
|
|
||||||
border-radius: 7px;
|
|
||||||
padding: 7px 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
QToolButton:hover {
|
|
||||||
background: #1b2531;
|
|
||||||
color: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
QTabWidget::pane {
|
|
||||||
border: 1px solid #202a36;
|
|
||||||
background: #0d1117;
|
|
||||||
}
|
|
||||||
|
|
||||||
QTabBar::tab {
|
|
||||||
background: #111820;
|
|
||||||
color: #7d8b99;
|
|
||||||
border: 1px solid #202a36;
|
|
||||||
border-bottom: none;
|
|
||||||
padding: 9px 16px;
|
|
||||||
margin-right: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
QTabBar::tab:selected {
|
|
||||||
background: #17212c;
|
|
||||||
color: #f0f6fc;
|
|
||||||
}
|
|
||||||
|
|
||||||
QDockWidget {
|
|
||||||
color: #aeb9c6;
|
|
||||||
titlebar-close-icon: none;
|
|
||||||
titlebar-normal-icon: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
QDockWidget::title {
|
|
||||||
background: #111820;
|
|
||||||
border-bottom: 1px solid #202a36;
|
|
||||||
padding: 9px 11px;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
QFrame#editorCard, QFrame#bottomCard {
|
|
||||||
background: #111820;
|
|
||||||
border: 1px solid #202a36;
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
QPlainTextEdit {
|
|
||||||
background: #0b0f14;
|
|
||||||
color: #d6deea;
|
|
||||||
border: 1px solid #202a36;
|
|
||||||
border-radius: 9px;
|
|
||||||
selection-background-color: #244a7d;
|
|
||||||
padding: 8px;
|
|
||||||
font-family: "JetBrains Mono", "Consolas", monospace;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
QLineEdit#singleLine {
|
|
||||||
background: #0b0f14;
|
|
||||||
color: #f0f6fc;
|
|
||||||
border: 1px solid #303b48;
|
|
||||||
border-radius: 9px;
|
|
||||||
padding: 9px 11px;
|
|
||||||
font-family: "JetBrains Mono", "Consolas", monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
QLineEdit#singleLine:focus {
|
|
||||||
border: 1px solid #3b82f6;
|
|
||||||
}
|
|
||||||
|
|
||||||
QPushButton {
|
|
||||||
background: #2563eb;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 8px 13px;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
QPushButton:hover {
|
|
||||||
background: #3475ef;
|
|
||||||
}
|
|
||||||
|
|
||||||
QPushButton#operator {
|
|
||||||
background: #1b2531;
|
|
||||||
color: #cbd5e1;
|
|
||||||
border: 1px solid #293543;
|
|
||||||
min-width: 34px;
|
|
||||||
}
|
|
||||||
|
|
||||||
QPushButton#operator:hover {
|
|
||||||
background: #253344;
|
|
||||||
color: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
QPushButton#secondary {
|
|
||||||
background: #1b2531;
|
|
||||||
color: #b9c4d0;
|
|
||||||
border: 1px solid #293543;
|
|
||||||
}
|
|
||||||
|
|
||||||
QListWidget, QTableWidget {
|
|
||||||
background: #0d131a;
|
|
||||||
color: #cbd5e1;
|
|
||||||
border: 1px solid #202a36;
|
|
||||||
border-radius: 8px;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
QListWidget::item {
|
|
||||||
padding: 7px 8px;
|
|
||||||
border-radius: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
QListWidget::item:hover {
|
|
||||||
background: #17212c;
|
|
||||||
}
|
|
||||||
|
|
||||||
QListWidget::item:selected {
|
|
||||||
background: #1c3153;
|
|
||||||
color: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
QHeaderView::section {
|
|
||||||
background: #161f29;
|
|
||||||
color: #9aa8b7;
|
|
||||||
border: none;
|
|
||||||
border-right: 1px solid #202a36;
|
|
||||||
border-bottom: 1px solid #202a36;
|
|
||||||
padding: 7px;
|
|
||||||
}
|
|
||||||
|
|
||||||
QTableWidget::item {
|
|
||||||
padding: 6px;
|
|
||||||
border-bottom: 1px solid #18212b;
|
|
||||||
}
|
|
||||||
|
|
||||||
QStatusBar {
|
|
||||||
background: #0b0f14;
|
|
||||||
color: #7d8b99;
|
|
||||||
border-top: 1px solid #202a36;
|
|
||||||
}
|
|
||||||
|
|
||||||
QScrollBar:vertical {
|
|
||||||
background: #0d131a;
|
|
||||||
width: 9px;
|
|
||||||
}
|
|
||||||
|
|
||||||
QScrollBar::handle:vertical {
|
|
||||||
background: #303c4a;
|
|
||||||
border-radius: 4px;
|
|
||||||
min-height: 25px;
|
|
||||||
}
|
|
||||||
|
|
||||||
QScrollBar::add-line:vertical,
|
|
||||||
QScrollBar::sub-line:vertical {
|
|
||||||
height: 0;
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class AbacusHighlighter(QSyntaxHighlighter):
|
|
||||||
def __init__(self, document):
|
|
||||||
super().__init__(document)
|
|
||||||
|
|
||||||
self.number = QTextCharFormat()
|
|
||||||
self.number.setForeground(QColor("#79c0ff"))
|
|
||||||
|
|
||||||
self.operator = QTextCharFormat()
|
|
||||||
self.operator.setForeground(QColor("#ff7b72"))
|
|
||||||
|
|
||||||
self.comment = QTextCharFormat()
|
|
||||||
self.comment.setForeground(QColor("#6e7681"))
|
|
||||||
self.comment.setFontItalic(True)
|
|
||||||
|
|
||||||
self.assignment = QTextCharFormat()
|
|
||||||
self.assignment.setForeground(QColor("#d2a8ff"))
|
|
||||||
|
|
||||||
self.keyword = QTextCharFormat()
|
|
||||||
self.keyword.setForeground(QColor("#ffa657"))
|
|
||||||
|
|
||||||
def highlightBlock(self, text):
|
|
||||||
# Comments
|
|
||||||
comment_pos = text.find("#")
|
|
||||||
if comment_pos >= 0:
|
|
||||||
self.setFormat(comment_pos, len(text) - comment_pos, self.comment)
|
|
||||||
code = text[:comment_pos]
|
|
||||||
else:
|
|
||||||
code = text
|
|
||||||
|
|
||||||
# Numbers
|
|
||||||
i = 0
|
|
||||||
while i < len(code):
|
|
||||||
if code[i].isdigit() or (
|
|
||||||
code[i] == "." and i + 1 < len(code) and code[i + 1].isdigit()
|
|
||||||
):
|
|
||||||
start = i
|
|
||||||
i += 1
|
|
||||||
while i < len(code) and (code[i].isdigit() or code[i] == "."):
|
|
||||||
i += 1
|
|
||||||
self.setFormat(start, i - start, self.number)
|
|
||||||
else:
|
|
||||||
i += 1
|
|
||||||
|
|
||||||
for char in "+-*/=():[];":
|
|
||||||
start = 0
|
|
||||||
while True:
|
|
||||||
pos = code.find(char, start)
|
|
||||||
if pos < 0:
|
|
||||||
break
|
|
||||||
self.setFormat(pos, 1, self.operator)
|
|
||||||
start = pos + 1
|
|
||||||
|
|
||||||
# Function definitions: name before ':'
|
|
||||||
colon = code.find(":")
|
|
||||||
if colon > 0:
|
|
||||||
name = code[:colon].strip()
|
|
||||||
if name:
|
|
||||||
pos = code.find(name)
|
|
||||||
self.setFormat(pos, len(name), self.keyword)
|
|
||||||
|
|
||||||
|
|
||||||
class LineNumberArea(QWidget):
|
|
||||||
def __init__(self, editor):
|
|
||||||
super().__init__(editor)
|
|
||||||
self.editor = editor
|
|
||||||
|
|
||||||
def sizeHint(self):
|
|
||||||
return QSize(self.editor.line_number_width(), 0)
|
|
||||||
|
|
||||||
def paintEvent(self, event):
|
|
||||||
painter = QPainter(self)
|
|
||||||
painter.fillRect(event.rect(), QColor("#0f151c"))
|
|
||||||
|
|
||||||
block = self.editor.firstVisibleBlock()
|
|
||||||
block_number = block.blockNumber()
|
|
||||||
top = int(
|
|
||||||
self.editor.blockBoundingGeometry(block).translated(
|
|
||||||
self.editor.contentOffset()
|
|
||||||
).top()
|
|
||||||
)
|
|
||||||
bottom = top + int(self.editor.blockBoundingRect(block).height())
|
|
||||||
|
|
||||||
while block.isValid() and top <= event.rect().bottom():
|
|
||||||
if block.isVisible() and bottom >= event.rect().top():
|
|
||||||
painter.setPen(QColor("#596878"))
|
|
||||||
painter.drawText(
|
|
||||||
0,
|
|
||||||
top,
|
|
||||||
self.width() - 7,
|
|
||||||
self.editor.fontMetrics().height(),
|
|
||||||
Qt.AlignRight,
|
|
||||||
str(block_number + 1),
|
|
||||||
)
|
|
||||||
|
|
||||||
block = block.next()
|
|
||||||
top = bottom
|
|
||||||
bottom = top + int(self.editor.blockBoundingRect(block).height())
|
|
||||||
block_number += 1
|
|
||||||
|
|
||||||
|
|
||||||
class CodeEditor(QPlainTextEdit):
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__()
|
|
||||||
|
|
||||||
self.line_numbers = LineNumberArea(self)
|
|
||||||
self.blockCountChanged.connect(self.update_line_number_width)
|
|
||||||
self.updateRequest.connect(self.update_line_number_area)
|
|
||||||
self.cursorPositionChanged.connect(self.highlight_current_line)
|
|
||||||
|
|
||||||
self.update_line_number_width(0)
|
|
||||||
self.highlight_current_line()
|
|
||||||
|
|
||||||
font = QFont("JetBrains Mono", 11)
|
|
||||||
font.setStyleHint(QFont.Monospace)
|
|
||||||
self.setFont(font)
|
|
||||||
self.setTabStopDistance(
|
|
||||||
self.fontMetrics().horizontalAdvance(" ") * 4
|
|
||||||
)
|
|
||||||
|
|
||||||
def line_number_width(self):
|
|
||||||
digits = len(str(max(1, self.blockCount())))
|
|
||||||
return 12 + self.fontMetrics().horizontalAdvance("9") * digits
|
|
||||||
|
|
||||||
def update_line_number_width(self, _):
|
|
||||||
self.setViewportMargins(self.line_number_width(), 0, 0, 0)
|
|
||||||
|
|
||||||
def update_line_number_area(self, rect, dy):
|
|
||||||
if dy:
|
|
||||||
self.line_numbers.scroll(0, dy)
|
|
||||||
else:
|
|
||||||
self.line_numbers.update(0, rect.y(), self.line_numbers.width(), rect.height())
|
|
||||||
|
|
||||||
if rect.contains(self.viewport().rect()):
|
|
||||||
self.update_line_number_width(0)
|
|
||||||
|
|
||||||
def resizeEvent(self, event):
|
|
||||||
super().resizeEvent(event)
|
|
||||||
rect = self.contentsRect()
|
|
||||||
rect.setWidth(self.line_number_width())
|
|
||||||
self.line_numbers.setGeometry(rect)
|
|
||||||
|
|
||||||
def highlight_current_line(self):
|
|
||||||
extra = []
|
|
||||||
if not self.isReadOnly():
|
|
||||||
selection = QTextCharFormat()
|
|
||||||
selection.setBackground(QColor("#111a24"))
|
|
||||||
selection.setProperty(QTextCharFormat.FullWidthSelection, True)
|
|
||||||
extra.append((self.textCursor(), selection))
|
|
||||||
self.setExtraSelections(extra)
|
|
||||||
|
|
||||||
|
|
||||||
class ResultGraph(QWidget):
|
|
||||||
"""Small dependency-free graph of numeric results."""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__()
|
|
||||||
self.values = []
|
|
||||||
|
|
||||||
def set_values(self, values):
|
|
||||||
self.values = values[-50:]
|
|
||||||
self.update()
|
|
||||||
|
|
||||||
def paintEvent(self, event):
|
|
||||||
painter = QPainter(self)
|
|
||||||
painter.setRenderHint(QPainter.Antialiasing)
|
|
||||||
painter.fillRect(self.rect(), QColor("#0b0f14"))
|
|
||||||
|
|
||||||
r = self.rect().adjusted(45, 20, -20, -35)
|
|
||||||
painter.setPen(QColor("#273442"))
|
|
||||||
painter.drawRect(r)
|
|
||||||
|
|
||||||
if not self.values:
|
|
||||||
painter.setPen(QColor("#647383"))
|
|
||||||
painter.drawText(r, Qt.AlignCenter, "No numeric results yet")
|
|
||||||
return
|
|
||||||
|
|
||||||
numeric = [v for v in self.values if math.isfinite(v)]
|
|
||||||
if not numeric:
|
|
||||||
return
|
|
||||||
|
|
||||||
lo = min(numeric)
|
|
||||||
hi = max(numeric)
|
|
||||||
if abs(hi - lo) < 1e-12:
|
|
||||||
lo -= 1
|
|
||||||
hi += 1
|
|
||||||
|
|
||||||
painter.setPen(QColor("#344454"))
|
|
||||||
for frac in (0.0, 0.5, 1.0):
|
|
||||||
y = int(r.bottom() - frac * r.height())
|
|
||||||
painter.drawLine(r.left(), y, r.right(), y)
|
|
||||||
|
|
||||||
painter.setPen(QColor("#5f6f80"))
|
|
||||||
painter.drawText(5, r.top() + 5, 35, 20, Qt.AlignRight, f"{hi:g}")
|
|
||||||
painter.drawText(5, r.bottom() - 15, 35, 20, Qt.AlignRight, f"{lo:g}")
|
|
||||||
|
|
||||||
if len(numeric) == 1:
|
|
||||||
x = r.center().x()
|
|
||||||
y = r.bottom() - (numeric[0] - lo) / (hi - lo) * r.height()
|
|
||||||
painter.setBrush(QColor("#58a6ff"))
|
|
||||||
painter.drawEllipse(int(x - 4), int(y - 4), 8, 8)
|
|
||||||
return
|
|
||||||
|
|
||||||
points = []
|
|
||||||
count = len(numeric)
|
|
||||||
for i, value in enumerate(numeric):
|
|
||||||
x = r.left() + i * r.width() / max(1, count - 1)
|
|
||||||
y = r.bottom() - (value - lo) / (hi - lo) * r.height()
|
|
||||||
points.append((x, y))
|
|
||||||
|
|
||||||
painter.setPen(QColor("#58a6ff"))
|
|
||||||
for a, b in zip(points, points[1:]):
|
|
||||||
painter.drawLine(int(a[0]), int(a[1]), int(b[0]), int(b[1]))
|
|
||||||
|
|
||||||
painter.setBrush(QColor("#58a6ff"))
|
|
||||||
for x, y in points:
|
|
||||||
painter.drawEllipse(int(x - 3), int(y - 3), 6, 6)
|
|
||||||
|
|
||||||
|
|
||||||
class AbacusIDE(QMainWindow):
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__()
|
|
||||||
|
|
||||||
self.core = AbacusCore()
|
|
||||||
self.step = 0
|
|
||||||
self.results = []
|
|
||||||
|
|
||||||
self.setWindowTitle("Abacus IDE")
|
|
||||||
self.resize(1400, 850)
|
|
||||||
self.setMinimumSize(1050, 650)
|
|
||||||
|
|
||||||
self._build_toolbar()
|
|
||||||
self._build_main()
|
|
||||||
self._build_docks()
|
|
||||||
self._build_statusbar()
|
|
||||||
|
|
||||||
self._refresh_sidebars()
|
|
||||||
self.editor.setFocus()
|
|
||||||
|
|
||||||
def _build_toolbar(self):
|
|
||||||
toolbar = QToolBar()
|
|
||||||
toolbar.setMovable(False)
|
|
||||||
toolbar.setToolButtonStyle(Qt.ToolButtonTextOnly)
|
|
||||||
self.addToolBar(toolbar)
|
|
||||||
|
|
||||||
run = toolbar.addAction("▶ Run")
|
|
||||||
run.triggered.connect(self.run_document)
|
|
||||||
|
|
||||||
run_line = toolbar.addAction("Run line")
|
|
||||||
run_line.triggered.connect(self.run_current_line)
|
|
||||||
|
|
||||||
toolbar.addSeparator()
|
|
||||||
|
|
||||||
for symbol in ("+", "-", "*", "/", "(", ")", "[", "]", "="):
|
|
||||||
button = QPushButton(symbol)
|
|
||||||
button.setObjectName("operator")
|
|
||||||
button.clicked.connect(
|
|
||||||
lambda checked=False, s=symbol: self.insert_operator(s)
|
|
||||||
)
|
|
||||||
toolbar.addWidget(button)
|
|
||||||
|
|
||||||
toolbar.addSeparator()
|
|
||||||
|
|
||||||
clear = toolbar.addAction("Clear")
|
|
||||||
clear.triggered.connect(self.editor.clear)
|
|
||||||
|
|
||||||
toolbar.addSeparator()
|
|
||||||
|
|
||||||
reset = toolbar.addAction("Reset")
|
|
||||||
reset.triggered.connect(self.reset_abacus)
|
|
||||||
|
|
||||||
def _build_main(self):
|
|
||||||
self.tabs = QTabWidget()
|
|
||||||
self.setCentralWidget(self.tabs)
|
|
||||||
|
|
||||||
# Workspace tab
|
|
||||||
workspace = QWidget()
|
|
||||||
workspace_layout = QVBoxLayout(workspace)
|
|
||||||
workspace_layout.setContentsMargins(10, 10, 10, 10)
|
|
||||||
workspace_layout.setSpacing(8)
|
|
||||||
|
|
||||||
editor_header = QHBoxLayout()
|
|
||||||
label = QLabel("Expression editor")
|
|
||||||
label.setStyleSheet(
|
|
||||||
"font-size: 13px; font-weight: 700; color: #c9d1d9;"
|
|
||||||
)
|
|
||||||
|
|
||||||
hint = QLabel(
|
|
||||||
"One command per line • # comments • Enter = run line"
|
|
||||||
)
|
|
||||||
hint.setStyleSheet("color: #657585;")
|
|
||||||
|
|
||||||
editor_header.addWidget(label)
|
|
||||||
editor_header.addStretch()
|
|
||||||
editor_header.addWidget(hint)
|
|
||||||
workspace_layout.addLayout(editor_header)
|
|
||||||
|
|
||||||
self.editor = CodeEditor()
|
|
||||||
AbacusHighlighter(self.editor.document())
|
|
||||||
self.editor.setPlainText(
|
|
||||||
"# Example\n"
|
|
||||||
"alpha = 10\n"
|
|
||||||
"beta = 2\n"
|
|
||||||
"alpha + beta * 3\n"
|
|
||||||
)
|
|
||||||
workspace_layout.addWidget(self.editor, 1)
|
|
||||||
|
|
||||||
bottom = QFrame()
|
|
||||||
bottom.setObjectName("bottomCard")
|
|
||||||
bottom_layout = QHBoxLayout(bottom)
|
|
||||||
bottom_layout.setContentsMargins(10, 8, 10, 8)
|
|
||||||
|
|
||||||
bottom_layout.addWidget(QLabel("Quick input:"))
|
|
||||||
|
|
||||||
self.single_line = QLineEdit()
|
|
||||||
self.single_line.setObjectName("singleLine")
|
|
||||||
self.single_line.setPlaceholderText("Enter expression...")
|
|
||||||
self.single_line.returnPressed.connect(self.run_single_line)
|
|
||||||
bottom_layout.addWidget(self.single_line, 1)
|
|
||||||
|
|
||||||
execute = QPushButton("Execute")
|
|
||||||
execute.clicked.connect(self.run_single_line)
|
|
||||||
bottom_layout.addWidget(execute)
|
|
||||||
|
|
||||||
workspace_layout.addWidget(bottom)
|
|
||||||
|
|
||||||
self.tabs.addTab(workspace, "Workspace")
|
|
||||||
|
|
||||||
# Results tab
|
|
||||||
results_page = QWidget()
|
|
||||||
results_layout = QVBoxLayout(results_page)
|
|
||||||
results_layout.setContentsMargins(10, 10, 10, 10)
|
|
||||||
|
|
||||||
self.results_table = QTableWidget(0, 4)
|
|
||||||
self.results_table.setHorizontalHeaderLabels(
|
|
||||||
["Step", "Expression", "Result", "Comment"]
|
|
||||||
)
|
|
||||||
self.results_table.horizontalHeader().setStretchLastSection(True)
|
|
||||||
self.results_table.setAlternatingRowColors(False)
|
|
||||||
results_layout.addWidget(self.results_table)
|
|
||||||
|
|
||||||
self.tabs.addTab(results_page, "Results")
|
|
||||||
|
|
||||||
# Graph tab
|
|
||||||
graph_page = QWidget()
|
|
||||||
graph_layout = QVBoxLayout(graph_page)
|
|
||||||
graph_layout.setContentsMargins(10, 10, 10, 10)
|
|
||||||
|
|
||||||
graph_title = QLabel("Numeric result history")
|
|
||||||
graph_title.setStyleSheet(
|
|
||||||
"font-size: 13px; font-weight: 700; color: #c9d1d9;"
|
|
||||||
)
|
|
||||||
graph_layout.addWidget(graph_title)
|
|
||||||
|
|
||||||
self.graph = ResultGraph()
|
|
||||||
graph_layout.addWidget(self.graph, 1)
|
|
||||||
|
|
||||||
graph_info = QLabel(
|
|
||||||
"The graph uses numeric results from executed expressions."
|
|
||||||
)
|
|
||||||
graph_info.setStyleSheet("color: #657585;")
|
|
||||||
graph_layout.addWidget(graph_info)
|
|
||||||
|
|
||||||
self.tabs.addTab(graph_page, "Graph")
|
|
||||||
|
|
||||||
def _build_docks(self):
|
|
||||||
self.variables_dock = self._make_dock(
|
|
||||||
"Variables", self._make_list_widget()
|
|
||||||
)
|
|
||||||
self.functions_dock = self._make_dock(
|
|
||||||
"Functions", self._make_list_widget()
|
|
||||||
)
|
|
||||||
self.history_dock = self._make_dock(
|
|
||||||
"History", self._make_list_widget()
|
|
||||||
)
|
|
||||||
|
|
||||||
self.variables_list = self.variables_dock.widget()
|
|
||||||
self.functions_list = self.functions_dock.widget()
|
|
||||||
self.history_list = self.history_dock.widget()
|
|
||||||
|
|
||||||
self.addDockWidget(Qt.RightDockWidgetArea, self.variables_dock)
|
|
||||||
self.addDockWidget(Qt.RightDockWidgetArea, self.functions_dock)
|
|
||||||
self.addDockWidget(Qt.RightDockWidgetArea, self.history_dock)
|
|
||||||
|
|
||||||
self.tabifyDockWidget(self.variables_dock, self.functions_dock)
|
|
||||||
self.tabifyDockWidget(self.functions_dock, self.history_dock)
|
|
||||||
self.variables_dock.raise_()
|
|
||||||
|
|
||||||
def _make_list_widget(self):
|
|
||||||
return QListWidget()
|
|
||||||
|
|
||||||
def _make_dock(self, title, widget):
|
|
||||||
dock = QDockWidget(title, self)
|
|
||||||
dock.setAllowedAreas(
|
|
||||||
Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea
|
|
||||||
)
|
|
||||||
dock.setFeatures(
|
|
||||||
QDockWidget.DockWidgetMovable
|
|
||||||
| QDockWidget.DockWidgetFloatable
|
|
||||||
)
|
|
||||||
dock.setWidget(widget)
|
|
||||||
return dock
|
|
||||||
|
|
||||||
def _build_statusbar(self):
|
|
||||||
self.status = QStatusBar()
|
|
||||||
self.setStatusBar(self.status)
|
|
||||||
self.status.showMessage("Ready")
|
|
||||||
|
|
||||||
def insert_operator(self, symbol):
|
|
||||||
self.editor.insertPlainText(symbol)
|
|
||||||
self.editor.setFocus()
|
|
||||||
|
|
||||||
def run_single_line(self):
|
|
||||||
text = self.single_line.text().strip()
|
|
||||||
if not text:
|
|
||||||
return
|
|
||||||
|
|
||||||
self._execute(text)
|
|
||||||
self.single_line.clear()
|
|
||||||
|
|
||||||
def run_current_line(self):
|
|
||||||
cursor = self.editor.textCursor()
|
|
||||||
cursor.select(cursor.LineUnderCursor)
|
|
||||||
line = cursor.selectedText().strip()
|
|
||||||
|
|
||||||
if line:
|
|
||||||
self._execute(line)
|
|
||||||
|
|
||||||
def run_document(self):
|
|
||||||
lines = self.editor.toPlainText().splitlines()
|
|
||||||
|
|
||||||
executed = 0
|
|
||||||
for line in lines:
|
|
||||||
line = line.strip()
|
|
||||||
if not line or line.startswith("#"):
|
|
||||||
continue
|
|
||||||
|
|
||||||
self._execute(line)
|
|
||||||
executed += 1
|
|
||||||
|
|
||||||
self.status.showMessage(f"Executed {executed} command(s)")
|
|
||||||
|
|
||||||
def _execute(self, text):
|
|
||||||
self.step += 1
|
|
||||||
|
|
||||||
try:
|
|
||||||
result = self.core.parse_input(text)
|
|
||||||
except Exception as exc:
|
|
||||||
self.step -= 1
|
|
||||||
self.status.showMessage(f"Error: {exc}")
|
|
||||||
self.history_list.addItem(f"ERROR {text} → {exc}")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Critical: assignments/functions return None, but their
|
|
||||||
# dictionaries are already changed. Refresh immediately.
|
|
||||||
self._refresh_sidebars()
|
|
||||||
|
|
||||||
if not result:
|
|
||||||
self.status.showMessage(f"Updated: {text}")
|
|
||||||
return
|
|
||||||
|
|
||||||
original, _, value, comment = result
|
|
||||||
value_text = str(value)
|
|
||||||
comment_text = comment or ""
|
|
||||||
|
|
||||||
self.results.append(
|
|
||||||
{
|
|
||||||
"step": self.step,
|
|
||||||
"expression": original,
|
|
||||||
"result": value_text,
|
|
||||||
"comment": comment_text,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.history_list.addItem(
|
|
||||||
f"{self.step}. {original} = {value_text}"
|
|
||||||
)
|
|
||||||
self.history_list.scrollToBottom()
|
|
||||||
|
|
||||||
self._add_result_row(
|
|
||||||
self.step, original, value_text, comment_text
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
numeric = float(value)
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
numeric = None
|
|
||||||
|
|
||||||
if numeric is not None and math.isfinite(numeric):
|
|
||||||
self.graph.values.append(numeric)
|
|
||||||
self.graph.set_values(self.graph.values)
|
|
||||||
|
|
||||||
self.status.showMessage(f"Result: {value_text}")
|
|
||||||
|
|
||||||
def _add_result_row(self, step, expression, result, comment):
|
|
||||||
row = self.results_table.rowCount()
|
|
||||||
self.results_table.insertRow(row)
|
|
||||||
|
|
||||||
values = [str(step), expression, result, comment]
|
|
||||||
|
|
||||||
for col, value in enumerate(values):
|
|
||||||
self.results_table.setItem(
|
|
||||||
row, col, QTableWidgetItem(value)
|
|
||||||
)
|
|
||||||
|
|
||||||
self.results_table.resizeColumnsToContents()
|
|
||||||
self.results_table.scrollToBottom()
|
|
||||||
|
|
||||||
def _refresh_sidebars(self):
|
|
||||||
self.variables_list.clear()
|
|
||||||
|
|
||||||
for key, value in self.core.get_vars().items():
|
|
||||||
self.variables_list.addItem(f"{key} = {value}")
|
|
||||||
|
|
||||||
self.functions_list.clear()
|
|
||||||
|
|
||||||
for key, value in self.core.get_funcs().items():
|
|
||||||
self.functions_list.addItem(f"{key} = {value}")
|
|
||||||
|
|
||||||
def reset_abacus(self):
|
|
||||||
self.core = AbacusCore()
|
|
||||||
self.step = 0
|
|
||||||
self.results.clear()
|
|
||||||
|
|
||||||
self.variables_list.clear()
|
|
||||||
self.functions_list.clear()
|
|
||||||
self.history_list.clear()
|
|
||||||
self.results_table.setRowCount(0)
|
|
||||||
|
|
||||||
self.graph.set_values([])
|
|
||||||
self.status.showMessage("Abacus state reset")
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
app = QApplication(sys.argv)
|
|
||||||
app.setStyle("Fusion")
|
|
||||||
app.setStyleSheet(STYLE)
|
|
||||||
|
|
||||||
window = AbacusIDE()
|
|
||||||
window.show()
|
|
||||||
|
|
||||||
sys.exit(app.exec())
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
|
|
@ -1,774 +0,0 @@
|
||||||
import math
|
|
||||||
import sys
|
|
||||||
|
|
||||||
from PySide6.QtCore import Qt, QSize
|
|
||||||
from PySide6.QtGui import (
|
|
||||||
QColor,
|
|
||||||
QFont,
|
|
||||||
QPainter,
|
|
||||||
QSyntaxHighlighter,
|
|
||||||
QTextCharFormat,
|
|
||||||
)
|
|
||||||
from PySide6.QtWidgets import (
|
|
||||||
QApplication,
|
|
||||||
QDockWidget,
|
|
||||||
QFrame,
|
|
||||||
QHBoxLayout,
|
|
||||||
QLabel,
|
|
||||||
QLineEdit,
|
|
||||||
QListWidget,
|
|
||||||
QMainWindow,
|
|
||||||
QPlainTextEdit,
|
|
||||||
QTextEdit,
|
|
||||||
QPushButton,
|
|
||||||
QStatusBar,
|
|
||||||
QTabWidget,
|
|
||||||
QTableWidget,
|
|
||||||
QTableWidgetItem,
|
|
||||||
QToolBar,
|
|
||||||
QVBoxLayout,
|
|
||||||
QWidget,
|
|
||||||
)
|
|
||||||
|
|
||||||
from abacus_core import AbacusCore
|
|
||||||
|
|
||||||
|
|
||||||
STYLE = """
|
|
||||||
QMainWindow, QWidget {
|
|
||||||
background: #0d1117;
|
|
||||||
color: #e6edf3;
|
|
||||||
font-family: "Segoe UI";
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
QToolBar {
|
|
||||||
background: #111820;
|
|
||||||
border: 0;
|
|
||||||
border-bottom: 1px solid #202a36;
|
|
||||||
spacing: 5px;
|
|
||||||
padding: 6px 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
QToolButton {
|
|
||||||
color: #c9d1d9;
|
|
||||||
background: transparent;
|
|
||||||
border-radius: 7px;
|
|
||||||
padding: 7px 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
QToolButton:hover {
|
|
||||||
background: #1b2531;
|
|
||||||
color: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
QTabWidget::pane {
|
|
||||||
border: 1px solid #202a36;
|
|
||||||
background: #0d1117;
|
|
||||||
}
|
|
||||||
|
|
||||||
QTabBar::tab {
|
|
||||||
background: #111820;
|
|
||||||
color: #7d8b99;
|
|
||||||
border: 1px solid #202a36;
|
|
||||||
border-bottom: none;
|
|
||||||
padding: 9px 16px;
|
|
||||||
margin-right: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
QTabBar::tab:selected {
|
|
||||||
background: #17212c;
|
|
||||||
color: #f0f6fc;
|
|
||||||
}
|
|
||||||
|
|
||||||
QDockWidget {
|
|
||||||
color: #aeb9c6;
|
|
||||||
titlebar-close-icon: none;
|
|
||||||
titlebar-normal-icon: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
QDockWidget::title {
|
|
||||||
background: #111820;
|
|
||||||
border-bottom: 1px solid #202a36;
|
|
||||||
padding: 9px 11px;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
QFrame#editorCard, QFrame#bottomCard {
|
|
||||||
background: #111820;
|
|
||||||
border: 1px solid #202a36;
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
QPlainTextEdit {
|
|
||||||
background: #0b0f14;
|
|
||||||
color: #d6deea;
|
|
||||||
border: 1px solid #202a36;
|
|
||||||
border-radius: 9px;
|
|
||||||
selection-background-color: #244a7d;
|
|
||||||
padding: 8px;
|
|
||||||
font-family: "JetBrains Mono", "Consolas", monospace;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
QLineEdit#singleLine {
|
|
||||||
background: #0b0f14;
|
|
||||||
color: #f0f6fc;
|
|
||||||
border: 1px solid #303b48;
|
|
||||||
border-radius: 9px;
|
|
||||||
padding: 9px 11px;
|
|
||||||
font-family: "JetBrains Mono", "Consolas", monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
QLineEdit#singleLine:focus {
|
|
||||||
border: 1px solid #3b82f6;
|
|
||||||
}
|
|
||||||
|
|
||||||
QPushButton {
|
|
||||||
background: #2563eb;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 8px 13px;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
QPushButton:hover {
|
|
||||||
background: #3475ef;
|
|
||||||
}
|
|
||||||
|
|
||||||
QPushButton#operator {
|
|
||||||
background: #1b2531;
|
|
||||||
color: #cbd5e1;
|
|
||||||
border: 1px solid #293543;
|
|
||||||
min-width: 34px;
|
|
||||||
}
|
|
||||||
|
|
||||||
QPushButton#operator:hover {
|
|
||||||
background: #253344;
|
|
||||||
color: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
QPushButton#secondary {
|
|
||||||
background: #1b2531;
|
|
||||||
color: #b9c4d0;
|
|
||||||
border: 1px solid #293543;
|
|
||||||
}
|
|
||||||
|
|
||||||
QListWidget, QTableWidget {
|
|
||||||
background: #0d131a;
|
|
||||||
color: #cbd5e1;
|
|
||||||
border: 1px solid #202a36;
|
|
||||||
border-radius: 8px;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
QListWidget::item {
|
|
||||||
padding: 7px 8px;
|
|
||||||
border-radius: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
QListWidget::item:hover {
|
|
||||||
background: #17212c;
|
|
||||||
}
|
|
||||||
|
|
||||||
QListWidget::item:selected {
|
|
||||||
background: #1c3153;
|
|
||||||
color: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
QHeaderView::section {
|
|
||||||
background: #161f29;
|
|
||||||
color: #9aa8b7;
|
|
||||||
border: none;
|
|
||||||
border-right: 1px solid #202a36;
|
|
||||||
border-bottom: 1px solid #202a36;
|
|
||||||
padding: 7px;
|
|
||||||
}
|
|
||||||
|
|
||||||
QTableWidget::item {
|
|
||||||
padding: 6px;
|
|
||||||
border-bottom: 1px solid #18212b;
|
|
||||||
}
|
|
||||||
|
|
||||||
QStatusBar {
|
|
||||||
background: #0b0f14;
|
|
||||||
color: #7d8b99;
|
|
||||||
border-top: 1px solid #202a36;
|
|
||||||
}
|
|
||||||
|
|
||||||
QScrollBar:vertical {
|
|
||||||
background: #0d131a;
|
|
||||||
width: 9px;
|
|
||||||
}
|
|
||||||
|
|
||||||
QScrollBar::handle:vertical {
|
|
||||||
background: #303c4a;
|
|
||||||
border-radius: 4px;
|
|
||||||
min-height: 25px;
|
|
||||||
}
|
|
||||||
|
|
||||||
QScrollBar::add-line:vertical,
|
|
||||||
QScrollBar::sub-line:vertical {
|
|
||||||
height: 0;
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class AbacusHighlighter(QSyntaxHighlighter):
|
|
||||||
def __init__(self, document):
|
|
||||||
super().__init__(document)
|
|
||||||
|
|
||||||
self.number = QTextCharFormat()
|
|
||||||
self.number.setForeground(QColor("#79c0ff"))
|
|
||||||
|
|
||||||
self.operator = QTextCharFormat()
|
|
||||||
self.operator.setForeground(QColor("#ff7b72"))
|
|
||||||
|
|
||||||
self.comment = QTextCharFormat()
|
|
||||||
self.comment.setForeground(QColor("#6e7681"))
|
|
||||||
self.comment.setFontItalic(True)
|
|
||||||
|
|
||||||
self.assignment = QTextCharFormat()
|
|
||||||
self.assignment.setForeground(QColor("#d2a8ff"))
|
|
||||||
|
|
||||||
self.keyword = QTextCharFormat()
|
|
||||||
self.keyword.setForeground(QColor("#ffa657"))
|
|
||||||
|
|
||||||
def highlightBlock(self, text):
|
|
||||||
# Comments
|
|
||||||
comment_pos = text.find("#")
|
|
||||||
if comment_pos >= 0:
|
|
||||||
self.setFormat(comment_pos, len(text) - comment_pos, self.comment)
|
|
||||||
code = text[:comment_pos]
|
|
||||||
else:
|
|
||||||
code = text
|
|
||||||
|
|
||||||
# Numbers
|
|
||||||
i = 0
|
|
||||||
while i < len(code):
|
|
||||||
if code[i].isdigit() or (
|
|
||||||
code[i] == "." and i + 1 < len(code) and code[i + 1].isdigit()
|
|
||||||
):
|
|
||||||
start = i
|
|
||||||
i += 1
|
|
||||||
while i < len(code) and (code[i].isdigit() or code[i] == "."):
|
|
||||||
i += 1
|
|
||||||
self.setFormat(start, i - start, self.number)
|
|
||||||
else:
|
|
||||||
i += 1
|
|
||||||
|
|
||||||
for char in "+-*/=():[];":
|
|
||||||
start = 0
|
|
||||||
while True:
|
|
||||||
pos = code.find(char, start)
|
|
||||||
if pos < 0:
|
|
||||||
break
|
|
||||||
self.setFormat(pos, 1, self.operator)
|
|
||||||
start = pos + 1
|
|
||||||
|
|
||||||
# Function definitions: name before ':'
|
|
||||||
colon = code.find(":")
|
|
||||||
if colon > 0:
|
|
||||||
name = code[:colon].strip()
|
|
||||||
if name:
|
|
||||||
pos = code.find(name)
|
|
||||||
self.setFormat(pos, len(name), self.keyword)
|
|
||||||
|
|
||||||
|
|
||||||
class LineNumberArea(QWidget):
|
|
||||||
def __init__(self, editor):
|
|
||||||
super().__init__(editor)
|
|
||||||
self.editor = editor
|
|
||||||
|
|
||||||
def sizeHint(self):
|
|
||||||
return QSize(self.editor.line_number_width(), 0)
|
|
||||||
|
|
||||||
def paintEvent(self, event):
|
|
||||||
painter = QPainter(self)
|
|
||||||
painter.fillRect(event.rect(), QColor("#0f151c"))
|
|
||||||
|
|
||||||
block = self.editor.firstVisibleBlock()
|
|
||||||
block_number = block.blockNumber()
|
|
||||||
top = int(
|
|
||||||
self.editor.blockBoundingGeometry(block).translated(
|
|
||||||
self.editor.contentOffset()
|
|
||||||
).top()
|
|
||||||
)
|
|
||||||
bottom = top + int(self.editor.blockBoundingRect(block).height())
|
|
||||||
|
|
||||||
while block.isValid() and top <= event.rect().bottom():
|
|
||||||
if block.isVisible() and bottom >= event.rect().top():
|
|
||||||
painter.setPen(QColor("#596878"))
|
|
||||||
painter.drawText(
|
|
||||||
0,
|
|
||||||
top,
|
|
||||||
self.width() - 7,
|
|
||||||
self.editor.fontMetrics().height(),
|
|
||||||
Qt.AlignRight,
|
|
||||||
str(block_number + 1),
|
|
||||||
)
|
|
||||||
|
|
||||||
block = block.next()
|
|
||||||
top = bottom
|
|
||||||
bottom = top + int(self.editor.blockBoundingRect(block).height())
|
|
||||||
block_number += 1
|
|
||||||
|
|
||||||
|
|
||||||
class CodeEditor(QPlainTextEdit):
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__()
|
|
||||||
|
|
||||||
self.line_numbers = LineNumberArea(self)
|
|
||||||
self.blockCountChanged.connect(self.update_line_number_width)
|
|
||||||
self.updateRequest.connect(self.update_line_number_area)
|
|
||||||
self.cursorPositionChanged.connect(self.highlight_current_line)
|
|
||||||
|
|
||||||
self.update_line_number_width(0)
|
|
||||||
self.highlight_current_line()
|
|
||||||
|
|
||||||
font = QFont("JetBrains Mono", 11)
|
|
||||||
font.setStyleHint(QFont.Monospace)
|
|
||||||
self.setFont(font)
|
|
||||||
self.setTabStopDistance(
|
|
||||||
self.fontMetrics().horizontalAdvance(" ") * 4
|
|
||||||
)
|
|
||||||
|
|
||||||
def line_number_width(self):
|
|
||||||
digits = len(str(max(1, self.blockCount())))
|
|
||||||
return 12 + self.fontMetrics().horizontalAdvance("9") * digits
|
|
||||||
|
|
||||||
def update_line_number_width(self, _):
|
|
||||||
self.setViewportMargins(self.line_number_width(), 0, 0, 0)
|
|
||||||
|
|
||||||
def update_line_number_area(self, rect, dy):
|
|
||||||
if dy:
|
|
||||||
self.line_numbers.scroll(0, dy)
|
|
||||||
else:
|
|
||||||
self.line_numbers.update(0, rect.y(), self.line_numbers.width(), rect.height())
|
|
||||||
|
|
||||||
if rect.contains(self.viewport().rect()):
|
|
||||||
self.update_line_number_width(0)
|
|
||||||
|
|
||||||
def resizeEvent(self, event):
|
|
||||||
super().resizeEvent(event)
|
|
||||||
rect = self.contentsRect()
|
|
||||||
rect.setWidth(self.line_number_width())
|
|
||||||
self.line_numbers.setGeometry(rect)
|
|
||||||
|
|
||||||
def highlight_current_line(self):
|
|
||||||
extra = []
|
|
||||||
if not self.isReadOnly():
|
|
||||||
selection = QTextCharFormat()
|
|
||||||
selection.setBackground(QColor("#111a24"))
|
|
||||||
selection.setProperty(QTextCharFormat.FullWidthSelection, True)
|
|
||||||
extra_selection = QTextEdit.ExtraSelection()
|
|
||||||
extra_selection.cursor = self.textCursor()
|
|
||||||
extra_selection.format = selection
|
|
||||||
extra.append(extra_selection)
|
|
||||||
self.setExtraSelections(extra)
|
|
||||||
|
|
||||||
|
|
||||||
class ResultGraph(QWidget):
|
|
||||||
"""Small dependency-free graph of numeric results."""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__()
|
|
||||||
self.values = []
|
|
||||||
|
|
||||||
def set_values(self, values):
|
|
||||||
self.values = values[-50:]
|
|
||||||
self.update()
|
|
||||||
|
|
||||||
def paintEvent(self, event):
|
|
||||||
painter = QPainter(self)
|
|
||||||
painter.setRenderHint(QPainter.Antialiasing)
|
|
||||||
painter.fillRect(self.rect(), QColor("#0b0f14"))
|
|
||||||
|
|
||||||
r = self.rect().adjusted(45, 20, -20, -35)
|
|
||||||
painter.setPen(QColor("#273442"))
|
|
||||||
painter.drawRect(r)
|
|
||||||
|
|
||||||
if not self.values:
|
|
||||||
painter.setPen(QColor("#647383"))
|
|
||||||
painter.drawText(r, Qt.AlignCenter, "No numeric results yet")
|
|
||||||
return
|
|
||||||
|
|
||||||
numeric = [v for v in self.values if math.isfinite(v)]
|
|
||||||
if not numeric:
|
|
||||||
return
|
|
||||||
|
|
||||||
lo = min(numeric)
|
|
||||||
hi = max(numeric)
|
|
||||||
if abs(hi - lo) < 1e-12:
|
|
||||||
lo -= 1
|
|
||||||
hi += 1
|
|
||||||
|
|
||||||
painter.setPen(QColor("#344454"))
|
|
||||||
for frac in (0.0, 0.5, 1.0):
|
|
||||||
y = int(r.bottom() - frac * r.height())
|
|
||||||
painter.drawLine(r.left(), y, r.right(), y)
|
|
||||||
|
|
||||||
painter.setPen(QColor("#5f6f80"))
|
|
||||||
painter.drawText(5, r.top() + 5, 35, 20, Qt.AlignRight, f"{hi:g}")
|
|
||||||
painter.drawText(5, r.bottom() - 15, 35, 20, Qt.AlignRight, f"{lo:g}")
|
|
||||||
|
|
||||||
if len(numeric) == 1:
|
|
||||||
x = r.center().x()
|
|
||||||
y = r.bottom() - (numeric[0] - lo) / (hi - lo) * r.height()
|
|
||||||
painter.setBrush(QColor("#58a6ff"))
|
|
||||||
painter.drawEllipse(int(x - 4), int(y - 4), 8, 8)
|
|
||||||
return
|
|
||||||
|
|
||||||
points = []
|
|
||||||
count = len(numeric)
|
|
||||||
for i, value in enumerate(numeric):
|
|
||||||
x = r.left() + i * r.width() / max(1, count - 1)
|
|
||||||
y = r.bottom() - (value - lo) / (hi - lo) * r.height()
|
|
||||||
points.append((x, y))
|
|
||||||
|
|
||||||
painter.setPen(QColor("#58a6ff"))
|
|
||||||
for a, b in zip(points, points[1:]):
|
|
||||||
painter.drawLine(int(a[0]), int(a[1]), int(b[0]), int(b[1]))
|
|
||||||
|
|
||||||
painter.setBrush(QColor("#58a6ff"))
|
|
||||||
for x, y in points:
|
|
||||||
painter.drawEllipse(int(x - 3), int(y - 3), 6, 6)
|
|
||||||
|
|
||||||
|
|
||||||
class AbacusIDE(QMainWindow):
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__()
|
|
||||||
|
|
||||||
self.core = AbacusCore()
|
|
||||||
self.step = 0
|
|
||||||
self.results = []
|
|
||||||
|
|
||||||
self.setWindowTitle("Abacus IDE")
|
|
||||||
self.resize(1400, 850)
|
|
||||||
self.setMinimumSize(1050, 650)
|
|
||||||
|
|
||||||
self._build_main()
|
|
||||||
self._build_toolbar()
|
|
||||||
self._build_docks()
|
|
||||||
self._build_statusbar()
|
|
||||||
|
|
||||||
self._refresh_sidebars()
|
|
||||||
self.editor.setFocus()
|
|
||||||
|
|
||||||
def _build_toolbar(self):
|
|
||||||
toolbar = QToolBar()
|
|
||||||
toolbar.setMovable(False)
|
|
||||||
toolbar.setToolButtonStyle(Qt.ToolButtonTextOnly)
|
|
||||||
self.addToolBar(toolbar)
|
|
||||||
|
|
||||||
run = toolbar.addAction("▶ Run")
|
|
||||||
run.triggered.connect(self.run_document)
|
|
||||||
|
|
||||||
run_line = toolbar.addAction("Run line")
|
|
||||||
run_line.triggered.connect(self.run_current_line)
|
|
||||||
|
|
||||||
toolbar.addSeparator()
|
|
||||||
|
|
||||||
for symbol in ("+", "-", "*", "/", "(", ")", "[", "]", "="):
|
|
||||||
button = QPushButton(symbol)
|
|
||||||
button.setObjectName("operator")
|
|
||||||
button.clicked.connect(
|
|
||||||
lambda checked=False, s=symbol: self.insert_operator(s)
|
|
||||||
)
|
|
||||||
toolbar.addWidget(button)
|
|
||||||
|
|
||||||
toolbar.addSeparator()
|
|
||||||
|
|
||||||
clear = toolbar.addAction("Clear")
|
|
||||||
clear.triggered.connect(self.editor.clear)
|
|
||||||
|
|
||||||
toolbar.addSeparator()
|
|
||||||
|
|
||||||
reset = toolbar.addAction("Reset")
|
|
||||||
reset.triggered.connect(self.reset_abacus)
|
|
||||||
|
|
||||||
def _build_main(self):
|
|
||||||
self.tabs = QTabWidget()
|
|
||||||
self.setCentralWidget(self.tabs)
|
|
||||||
|
|
||||||
# Workspace tab
|
|
||||||
workspace = QWidget()
|
|
||||||
workspace_layout = QVBoxLayout(workspace)
|
|
||||||
workspace_layout.setContentsMargins(10, 10, 10, 10)
|
|
||||||
workspace_layout.setSpacing(8)
|
|
||||||
|
|
||||||
editor_header = QHBoxLayout()
|
|
||||||
label = QLabel("Expression editor")
|
|
||||||
label.setStyleSheet(
|
|
||||||
"font-size: 13px; font-weight: 700; color: #c9d1d9;"
|
|
||||||
)
|
|
||||||
|
|
||||||
hint = QLabel(
|
|
||||||
"One command per line • # comments • Enter = run line"
|
|
||||||
)
|
|
||||||
hint.setStyleSheet("color: #657585;")
|
|
||||||
|
|
||||||
editor_header.addWidget(label)
|
|
||||||
editor_header.addStretch()
|
|
||||||
editor_header.addWidget(hint)
|
|
||||||
workspace_layout.addLayout(editor_header)
|
|
||||||
|
|
||||||
self.editor = CodeEditor()
|
|
||||||
self.highlighter = AbacusHighlighter(self.editor.document())
|
|
||||||
self.editor.setPlainText(
|
|
||||||
"# Example\n"
|
|
||||||
"alpha = 10\n"
|
|
||||||
"beta = 2\n"
|
|
||||||
"alpha + beta * 3\n"
|
|
||||||
)
|
|
||||||
workspace_layout.addWidget(self.editor, 1)
|
|
||||||
|
|
||||||
bottom = QFrame()
|
|
||||||
bottom.setObjectName("bottomCard")
|
|
||||||
bottom_layout = QHBoxLayout(bottom)
|
|
||||||
bottom_layout.setContentsMargins(10, 8, 10, 8)
|
|
||||||
|
|
||||||
bottom_layout.addWidget(QLabel("Quick input:"))
|
|
||||||
|
|
||||||
self.single_line = QLineEdit()
|
|
||||||
self.single_line.setObjectName("singleLine")
|
|
||||||
self.single_line.setPlaceholderText("Enter expression...")
|
|
||||||
self.single_line.returnPressed.connect(self.run_single_line)
|
|
||||||
bottom_layout.addWidget(self.single_line, 1)
|
|
||||||
|
|
||||||
execute = QPushButton("Execute")
|
|
||||||
execute.clicked.connect(self.run_single_line)
|
|
||||||
bottom_layout.addWidget(execute)
|
|
||||||
|
|
||||||
workspace_layout.addWidget(bottom)
|
|
||||||
|
|
||||||
self.tabs.addTab(workspace, "Workspace")
|
|
||||||
|
|
||||||
# Results tab
|
|
||||||
results_page = QWidget()
|
|
||||||
results_layout = QVBoxLayout(results_page)
|
|
||||||
results_layout.setContentsMargins(10, 10, 10, 10)
|
|
||||||
|
|
||||||
self.results_table = QTableWidget(0, 4)
|
|
||||||
self.results_table.setHorizontalHeaderLabels(
|
|
||||||
["Step", "Expression", "Result", "Comment"]
|
|
||||||
)
|
|
||||||
self.results_table.horizontalHeader().setStretchLastSection(True)
|
|
||||||
self.results_table.setAlternatingRowColors(False)
|
|
||||||
results_layout.addWidget(self.results_table)
|
|
||||||
|
|
||||||
self.tabs.addTab(results_page, "Results")
|
|
||||||
|
|
||||||
# Graph tab
|
|
||||||
graph_page = QWidget()
|
|
||||||
graph_layout = QVBoxLayout(graph_page)
|
|
||||||
graph_layout.setContentsMargins(10, 10, 10, 10)
|
|
||||||
|
|
||||||
graph_title = QLabel("Numeric result history")
|
|
||||||
graph_title.setStyleSheet(
|
|
||||||
"font-size: 13px; font-weight: 700; color: #c9d1d9;"
|
|
||||||
)
|
|
||||||
graph_layout.addWidget(graph_title)
|
|
||||||
|
|
||||||
self.graph = ResultGraph()
|
|
||||||
graph_layout.addWidget(self.graph, 1)
|
|
||||||
|
|
||||||
graph_info = QLabel(
|
|
||||||
"The graph uses numeric results from executed expressions."
|
|
||||||
)
|
|
||||||
graph_info.setStyleSheet("color: #657585;")
|
|
||||||
graph_layout.addWidget(graph_info)
|
|
||||||
|
|
||||||
self.tabs.addTab(graph_page, "Graph")
|
|
||||||
|
|
||||||
def _build_docks(self):
|
|
||||||
self.variables_dock = self._make_dock(
|
|
||||||
"Variables", self._make_list_widget()
|
|
||||||
)
|
|
||||||
self.functions_dock = self._make_dock(
|
|
||||||
"Functions", self._make_list_widget()
|
|
||||||
)
|
|
||||||
self.history_dock = self._make_dock(
|
|
||||||
"History", self._make_list_widget()
|
|
||||||
)
|
|
||||||
|
|
||||||
self.variables_list = self.variables_dock.widget()
|
|
||||||
self.functions_list = self.functions_dock.widget()
|
|
||||||
self.history_list = self.history_dock.widget()
|
|
||||||
|
|
||||||
self.addDockWidget(Qt.RightDockWidgetArea, self.variables_dock)
|
|
||||||
self.addDockWidget(Qt.RightDockWidgetArea, self.functions_dock)
|
|
||||||
self.addDockWidget(Qt.RightDockWidgetArea, self.history_dock)
|
|
||||||
|
|
||||||
self.tabifyDockWidget(self.variables_dock, self.functions_dock)
|
|
||||||
self.tabifyDockWidget(self.functions_dock, self.history_dock)
|
|
||||||
self.variables_dock.raise_()
|
|
||||||
|
|
||||||
def _make_list_widget(self):
|
|
||||||
return QListWidget()
|
|
||||||
|
|
||||||
def _make_dock(self, title, widget):
|
|
||||||
dock = QDockWidget(title, self)
|
|
||||||
dock.setAllowedAreas(
|
|
||||||
Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea
|
|
||||||
)
|
|
||||||
dock.setFeatures(
|
|
||||||
QDockWidget.DockWidgetMovable
|
|
||||||
| QDockWidget.DockWidgetFloatable
|
|
||||||
)
|
|
||||||
dock.setWidget(widget)
|
|
||||||
return dock
|
|
||||||
|
|
||||||
def _build_statusbar(self):
|
|
||||||
self.status = QStatusBar()
|
|
||||||
self.setStatusBar(self.status)
|
|
||||||
self.status.showMessage("Ready")
|
|
||||||
|
|
||||||
def insert_operator(self, symbol):
|
|
||||||
self.editor.insertPlainText(symbol)
|
|
||||||
self.editor.setFocus()
|
|
||||||
|
|
||||||
def run_single_line(self):
|
|
||||||
text = self.single_line.text().strip()
|
|
||||||
if not text:
|
|
||||||
return
|
|
||||||
|
|
||||||
self._execute(text)
|
|
||||||
self.single_line.clear()
|
|
||||||
|
|
||||||
def run_current_line(self):
|
|
||||||
cursor = self.editor.textCursor()
|
|
||||||
cursor.select(cursor.LineUnderCursor)
|
|
||||||
line = cursor.selectedText().strip()
|
|
||||||
|
|
||||||
if line:
|
|
||||||
self._execute(line)
|
|
||||||
|
|
||||||
def run_document(self):
|
|
||||||
lines = self.editor.toPlainText().splitlines()
|
|
||||||
|
|
||||||
executed = 0
|
|
||||||
failed = 0
|
|
||||||
|
|
||||||
for line in lines:
|
|
||||||
line = line.strip()
|
|
||||||
if not line or line.startswith("#"):
|
|
||||||
continue
|
|
||||||
|
|
||||||
if self._execute(line):
|
|
||||||
executed += 1
|
|
||||||
else:
|
|
||||||
failed += 1
|
|
||||||
|
|
||||||
if failed:
|
|
||||||
self.status.showMessage(
|
|
||||||
f"Executed {executed} command(s), {failed} error(s)"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
self.status.showMessage(f"Executed {executed} command(s)")
|
|
||||||
|
|
||||||
def _execute(self, text):
|
|
||||||
self.step += 1
|
|
||||||
|
|
||||||
try:
|
|
||||||
result = self.core.parse_input(text)
|
|
||||||
except Exception as exc:
|
|
||||||
self.step -= 1
|
|
||||||
self.status.showMessage(f"Error: {exc}")
|
|
||||||
self.history_list.addItem(f"ERROR {text} → {exc}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Critical: assignments/functions return None, but their
|
|
||||||
# dictionaries are already changed. Refresh immediately.
|
|
||||||
self._refresh_sidebars()
|
|
||||||
|
|
||||||
if not result:
|
|
||||||
self.status.showMessage(f"Updated: {text}")
|
|
||||||
return True
|
|
||||||
|
|
||||||
original, _, value, comment = result
|
|
||||||
value_text = str(value)
|
|
||||||
comment_text = comment or ""
|
|
||||||
|
|
||||||
self.results.append(
|
|
||||||
{
|
|
||||||
"step": self.step,
|
|
||||||
"expression": original,
|
|
||||||
"result": value_text,
|
|
||||||
"comment": comment_text,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.history_list.addItem(
|
|
||||||
f"{self.step}. {original} = {value_text}"
|
|
||||||
)
|
|
||||||
self.history_list.scrollToBottom()
|
|
||||||
|
|
||||||
self._add_result_row(
|
|
||||||
self.step, original, value_text, comment_text
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
numeric = float(value)
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
numeric = None
|
|
||||||
|
|
||||||
if numeric is not None and math.isfinite(numeric):
|
|
||||||
self.graph.values.append(numeric)
|
|
||||||
self.graph.set_values(self.graph.values)
|
|
||||||
|
|
||||||
self.status.showMessage(f"Result: {value_text}")
|
|
||||||
return True
|
|
||||||
|
|
||||||
def _add_result_row(self, step, expression, result, comment):
|
|
||||||
row = self.results_table.rowCount()
|
|
||||||
self.results_table.insertRow(row)
|
|
||||||
|
|
||||||
values = [str(step), expression, result, comment]
|
|
||||||
|
|
||||||
for col, value in enumerate(values):
|
|
||||||
self.results_table.setItem(
|
|
||||||
row, col, QTableWidgetItem(value)
|
|
||||||
)
|
|
||||||
|
|
||||||
self.results_table.resizeColumnsToContents()
|
|
||||||
self.results_table.scrollToBottom()
|
|
||||||
|
|
||||||
def _refresh_sidebars(self):
|
|
||||||
self.variables_list.clear()
|
|
||||||
|
|
||||||
for key, value in self.core.get_vars().items():
|
|
||||||
self.variables_list.addItem(f"{key} = {value}")
|
|
||||||
|
|
||||||
self.functions_list.clear()
|
|
||||||
|
|
||||||
for key, value in self.core.get_funcs().items():
|
|
||||||
self.functions_list.addItem(f"{key} = {value}")
|
|
||||||
|
|
||||||
def reset_abacus(self):
|
|
||||||
self.core = AbacusCore()
|
|
||||||
self.step = 0
|
|
||||||
self.results.clear()
|
|
||||||
|
|
||||||
self.variables_list.clear()
|
|
||||||
self.functions_list.clear()
|
|
||||||
self.history_list.clear()
|
|
||||||
self.results_table.setRowCount(0)
|
|
||||||
|
|
||||||
self.graph.set_values([])
|
|
||||||
self.status.showMessage("Abacus state reset")
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
app = QApplication(sys.argv)
|
|
||||||
app.setStyle("Fusion")
|
|
||||||
app.setStyleSheet(STYLE)
|
|
||||||
|
|
||||||
window = AbacusIDE()
|
|
||||||
window.show()
|
|
||||||
|
|
||||||
sys.exit(app.exec())
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,761 +0,0 @@
|
||||||
import math
|
|
||||||
import sys
|
|
||||||
|
|
||||||
from PySide6.QtCore import Qt, QRect, QSize
|
|
||||||
from PySide6.QtGui import (
|
|
||||||
QColor,
|
|
||||||
QFont,
|
|
||||||
QPainter,
|
|
||||||
QSyntaxHighlighter,
|
|
||||||
QTextCharFormat,
|
|
||||||
)
|
|
||||||
from PySide6.QtWidgets import (
|
|
||||||
QApplication,
|
|
||||||
QDockWidget,
|
|
||||||
QFrame,
|
|
||||||
QHBoxLayout,
|
|
||||||
QLabel,
|
|
||||||
QLineEdit,
|
|
||||||
QListWidget,
|
|
||||||
QMainWindow,
|
|
||||||
QPlainTextEdit,
|
|
||||||
QPushButton,
|
|
||||||
QSplitter,
|
|
||||||
QStatusBar,
|
|
||||||
QTabWidget,
|
|
||||||
QTableWidget,
|
|
||||||
QTableWidgetItem,
|
|
||||||
QToolBar,
|
|
||||||
QVBoxLayout,
|
|
||||||
QWidget,
|
|
||||||
)
|
|
||||||
|
|
||||||
from abacus_core import AbacusCore
|
|
||||||
|
|
||||||
|
|
||||||
STYLE = """
|
|
||||||
QMainWindow, QWidget {
|
|
||||||
background: #0d1117;
|
|
||||||
color: #e6edf3;
|
|
||||||
font-family: "Segoe UI";
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
QToolBar {
|
|
||||||
background: #111820;
|
|
||||||
border: 0;
|
|
||||||
border-bottom: 1px solid #202a36;
|
|
||||||
spacing: 5px;
|
|
||||||
padding: 6px 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
QToolButton {
|
|
||||||
color: #c9d1d9;
|
|
||||||
background: transparent;
|
|
||||||
border-radius: 7px;
|
|
||||||
padding: 7px 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
QToolButton:hover {
|
|
||||||
background: #1b2531;
|
|
||||||
color: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
QTabWidget::pane {
|
|
||||||
border: 1px solid #202a36;
|
|
||||||
background: #0d1117;
|
|
||||||
}
|
|
||||||
|
|
||||||
QTabBar::tab {
|
|
||||||
background: #111820;
|
|
||||||
color: #7d8b99;
|
|
||||||
border: 1px solid #202a36;
|
|
||||||
border-bottom: none;
|
|
||||||
padding: 9px 16px;
|
|
||||||
margin-right: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
QTabBar::tab:selected {
|
|
||||||
background: #17212c;
|
|
||||||
color: #f0f6fc;
|
|
||||||
}
|
|
||||||
|
|
||||||
QDockWidget {
|
|
||||||
color: #aeb9c6;
|
|
||||||
titlebar-close-icon: none;
|
|
||||||
titlebar-normal-icon: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
QDockWidget::title {
|
|
||||||
background: #111820;
|
|
||||||
border-bottom: 1px solid #202a36;
|
|
||||||
padding: 9px 11px;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
QFrame#editorCard, QFrame#bottomCard {
|
|
||||||
background: #111820;
|
|
||||||
border: 1px solid #202a36;
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
QPlainTextEdit {
|
|
||||||
background: #0b0f14;
|
|
||||||
color: #d6deea;
|
|
||||||
border: 1px solid #202a36;
|
|
||||||
border-radius: 9px;
|
|
||||||
selection-background-color: #244a7d;
|
|
||||||
padding: 8px;
|
|
||||||
font-family: "JetBrains Mono", "Consolas", monospace;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
QLineEdit#singleLine {
|
|
||||||
background: #0b0f14;
|
|
||||||
color: #f0f6fc;
|
|
||||||
border: 1px solid #303b48;
|
|
||||||
border-radius: 9px;
|
|
||||||
padding: 9px 11px;
|
|
||||||
font-family: "JetBrains Mono", "Consolas", monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
QLineEdit#singleLine:focus {
|
|
||||||
border: 1px solid #3b82f6;
|
|
||||||
}
|
|
||||||
|
|
||||||
QPushButton {
|
|
||||||
background: #2563eb;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 8px 13px;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
QPushButton:hover {
|
|
||||||
background: #3475ef;
|
|
||||||
}
|
|
||||||
|
|
||||||
QPushButton#operator {
|
|
||||||
background: #1b2531;
|
|
||||||
color: #cbd5e1;
|
|
||||||
border: 1px solid #293543;
|
|
||||||
min-width: 34px;
|
|
||||||
}
|
|
||||||
|
|
||||||
QPushButton#operator:hover {
|
|
||||||
background: #253344;
|
|
||||||
color: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
QPushButton#secondary {
|
|
||||||
background: #1b2531;
|
|
||||||
color: #b9c4d0;
|
|
||||||
border: 1px solid #293543;
|
|
||||||
}
|
|
||||||
|
|
||||||
QListWidget, QTableWidget {
|
|
||||||
background: #0d131a;
|
|
||||||
color: #cbd5e1;
|
|
||||||
border: 1px solid #202a36;
|
|
||||||
border-radius: 8px;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
QListWidget::item {
|
|
||||||
padding: 7px 8px;
|
|
||||||
border-radius: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
QListWidget::item:hover {
|
|
||||||
background: #17212c;
|
|
||||||
}
|
|
||||||
|
|
||||||
QListWidget::item:selected {
|
|
||||||
background: #1c3153;
|
|
||||||
color: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
QHeaderView::section {
|
|
||||||
background: #161f29;
|
|
||||||
color: #9aa8b7;
|
|
||||||
border: none;
|
|
||||||
border-right: 1px solid #202a36;
|
|
||||||
border-bottom: 1px solid #202a36;
|
|
||||||
padding: 7px;
|
|
||||||
}
|
|
||||||
|
|
||||||
QTableWidget::item {
|
|
||||||
padding: 6px;
|
|
||||||
border-bottom: 1px solid #18212b;
|
|
||||||
}
|
|
||||||
|
|
||||||
QStatusBar {
|
|
||||||
background: #0b0f14;
|
|
||||||
color: #7d8b99;
|
|
||||||
border-top: 1px solid #202a36;
|
|
||||||
}
|
|
||||||
|
|
||||||
QScrollBar:vertical {
|
|
||||||
background: #0d131a;
|
|
||||||
width: 9px;
|
|
||||||
}
|
|
||||||
|
|
||||||
QScrollBar::handle:vertical {
|
|
||||||
background: #303c4a;
|
|
||||||
border-radius: 4px;
|
|
||||||
min-height: 25px;
|
|
||||||
}
|
|
||||||
|
|
||||||
QScrollBar::add-line:vertical,
|
|
||||||
QScrollBar::sub-line:vertical {
|
|
||||||
height: 0;
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class AbacusHighlighter(QSyntaxHighlighter):
|
|
||||||
def __init__(self, document):
|
|
||||||
super().__init__(document)
|
|
||||||
|
|
||||||
self.number = QTextCharFormat()
|
|
||||||
self.number.setForeground(QColor("#79c0ff"))
|
|
||||||
|
|
||||||
self.operator = QTextCharFormat()
|
|
||||||
self.operator.setForeground(QColor("#ff7b72"))
|
|
||||||
|
|
||||||
self.comment = QTextCharFormat()
|
|
||||||
self.comment.setForeground(QColor("#6e7681"))
|
|
||||||
self.comment.setFontItalic(True)
|
|
||||||
|
|
||||||
self.assignment = QTextCharFormat()
|
|
||||||
self.assignment.setForeground(QColor("#d2a8ff"))
|
|
||||||
|
|
||||||
self.keyword = QTextCharFormat()
|
|
||||||
self.keyword.setForeground(QColor("#ffa657"))
|
|
||||||
|
|
||||||
def highlightBlock(self, text):
|
|
||||||
# Comments
|
|
||||||
comment_pos = text.find("#")
|
|
||||||
if comment_pos >= 0:
|
|
||||||
self.setFormat(comment_pos, len(text) - comment_pos, self.comment)
|
|
||||||
code = text[:comment_pos]
|
|
||||||
else:
|
|
||||||
code = text
|
|
||||||
|
|
||||||
# Numbers
|
|
||||||
i = 0
|
|
||||||
while i < len(code):
|
|
||||||
if code[i].isdigit() or (
|
|
||||||
code[i] == "." and i + 1 < len(code) and code[i + 1].isdigit()
|
|
||||||
):
|
|
||||||
start = i
|
|
||||||
i += 1
|
|
||||||
while i < len(code) and (code[i].isdigit() or code[i] == "."):
|
|
||||||
i += 1
|
|
||||||
self.setFormat(start, i - start, self.number)
|
|
||||||
else:
|
|
||||||
i += 1
|
|
||||||
|
|
||||||
for char in "+-*/=():[];":
|
|
||||||
start = 0
|
|
||||||
while True:
|
|
||||||
pos = code.find(char, start)
|
|
||||||
if pos < 0:
|
|
||||||
break
|
|
||||||
self.setFormat(pos, 1, self.operator)
|
|
||||||
start = pos + 1
|
|
||||||
|
|
||||||
# Function definitions: name before ':'
|
|
||||||
colon = code.find(":")
|
|
||||||
if colon > 0:
|
|
||||||
name = code[:colon].strip()
|
|
||||||
if name:
|
|
||||||
pos = code.find(name)
|
|
||||||
self.setFormat(pos, len(name), self.keyword)
|
|
||||||
|
|
||||||
|
|
||||||
class LineNumberArea(QWidget):
|
|
||||||
def __init__(self, editor):
|
|
||||||
super().__init__(editor)
|
|
||||||
self.editor = editor
|
|
||||||
|
|
||||||
def sizeHint(self):
|
|
||||||
return QSize(self.editor.line_number_width(), 0)
|
|
||||||
|
|
||||||
def paintEvent(self, event):
|
|
||||||
painter = QPainter(self)
|
|
||||||
painter.fillRect(event.rect(), QColor("#0f151c"))
|
|
||||||
|
|
||||||
block = self.editor.firstVisibleBlock()
|
|
||||||
block_number = block.blockNumber()
|
|
||||||
top = int(
|
|
||||||
self.editor.blockBoundingGeometry(block).translated(
|
|
||||||
self.editor.contentOffset()
|
|
||||||
).top()
|
|
||||||
)
|
|
||||||
bottom = top + int(self.editor.blockBoundingRect(block).height())
|
|
||||||
|
|
||||||
while block.isValid() and top <= event.rect().bottom():
|
|
||||||
if block.isVisible() and bottom >= event.rect().top():
|
|
||||||
painter.setPen(QColor("#596878"))
|
|
||||||
painter.drawText(
|
|
||||||
0,
|
|
||||||
top,
|
|
||||||
self.width() - 7,
|
|
||||||
self.editor.fontMetrics().height(),
|
|
||||||
Qt.AlignRight,
|
|
||||||
str(block_number + 1),
|
|
||||||
)
|
|
||||||
|
|
||||||
block = block.next()
|
|
||||||
top = bottom
|
|
||||||
bottom = top + int(self.editor.blockBoundingRect(block).height())
|
|
||||||
block_number += 1
|
|
||||||
|
|
||||||
|
|
||||||
class CodeEditor(QPlainTextEdit):
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__()
|
|
||||||
|
|
||||||
self.line_numbers = LineNumberArea(self)
|
|
||||||
self.blockCountChanged.connect(self.update_line_number_width)
|
|
||||||
self.updateRequest.connect(self.update_line_number_area)
|
|
||||||
self.cursorPositionChanged.connect(self.highlight_current_line)
|
|
||||||
|
|
||||||
self.update_line_number_width(0)
|
|
||||||
self.highlight_current_line()
|
|
||||||
|
|
||||||
font = QFont("JetBrains Mono", 11)
|
|
||||||
font.setStyleHint(QFont.Monospace)
|
|
||||||
self.setFont(font)
|
|
||||||
self.setTabStopDistance(
|
|
||||||
self.fontMetrics().horizontalAdvance(" ") * 4
|
|
||||||
)
|
|
||||||
|
|
||||||
def line_number_width(self):
|
|
||||||
digits = len(str(max(1, self.blockCount())))
|
|
||||||
return 12 + self.fontMetrics().horizontalAdvance("9") * digits
|
|
||||||
|
|
||||||
def update_line_number_width(self, _):
|
|
||||||
self.setViewportMargins(self.line_number_width(), 0, 0, 0)
|
|
||||||
|
|
||||||
def update_line_number_area(self, rect, dy):
|
|
||||||
if dy:
|
|
||||||
self.line_numbers.scroll(0, dy)
|
|
||||||
else:
|
|
||||||
self.line_numbers.update(0, rect.y(), self.line_numbers.width(), rect.height())
|
|
||||||
|
|
||||||
if rect.contains(self.viewport().rect()):
|
|
||||||
self.update_line_number_width(0)
|
|
||||||
|
|
||||||
def resizeEvent(self, event):
|
|
||||||
super().resizeEvent(event)
|
|
||||||
rect = self.contentsRect()
|
|
||||||
rect.setWidth(self.line_number_width())
|
|
||||||
self.line_numbers.setGeometry(rect)
|
|
||||||
|
|
||||||
def highlight_current_line(self):
|
|
||||||
extra = []
|
|
||||||
if not self.isReadOnly():
|
|
||||||
selection = QTextCharFormat()
|
|
||||||
selection.setBackground(QColor("#111a24"))
|
|
||||||
selection.setProperty(QTextCharFormat.FullWidthSelection, True)
|
|
||||||
extra.append((self.textCursor(), selection))
|
|
||||||
self.setExtraSelections(extra)
|
|
||||||
|
|
||||||
|
|
||||||
class ResultGraph(QWidget):
|
|
||||||
"""Small dependency-free graph of numeric results."""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__()
|
|
||||||
self.values = []
|
|
||||||
|
|
||||||
def set_values(self, values):
|
|
||||||
self.values = values[-50:]
|
|
||||||
self.update()
|
|
||||||
|
|
||||||
def paintEvent(self, event):
|
|
||||||
painter = QPainter(self)
|
|
||||||
painter.setRenderHint(QPainter.Antialiasing)
|
|
||||||
painter.fillRect(self.rect(), QColor("#0b0f14"))
|
|
||||||
|
|
||||||
r = self.rect().adjusted(45, 20, -20, -35)
|
|
||||||
painter.setPen(QColor("#273442"))
|
|
||||||
painter.drawRect(r)
|
|
||||||
|
|
||||||
if not self.values:
|
|
||||||
painter.setPen(QColor("#647383"))
|
|
||||||
painter.drawText(r, Qt.AlignCenter, "No numeric results yet")
|
|
||||||
return
|
|
||||||
|
|
||||||
numeric = [v for v in self.values if math.isfinite(v)]
|
|
||||||
if not numeric:
|
|
||||||
return
|
|
||||||
|
|
||||||
lo = min(numeric)
|
|
||||||
hi = max(numeric)
|
|
||||||
if abs(hi - lo) < 1e-12:
|
|
||||||
lo -= 1
|
|
||||||
hi += 1
|
|
||||||
|
|
||||||
painter.setPen(QColor("#344454"))
|
|
||||||
for frac in (0.0, 0.5, 1.0):
|
|
||||||
y = int(r.bottom() - frac * r.height())
|
|
||||||
painter.drawLine(r.left(), y, r.right(), y)
|
|
||||||
|
|
||||||
painter.setPen(QColor("#5f6f80"))
|
|
||||||
painter.drawText(5, r.top() + 5, 35, 20, Qt.AlignRight, f"{hi:g}")
|
|
||||||
painter.drawText(5, r.bottom() - 15, 35, 20, Qt.AlignRight, f"{lo:g}")
|
|
||||||
|
|
||||||
if len(numeric) == 1:
|
|
||||||
x = r.center().x()
|
|
||||||
y = r.bottom() - (numeric[0] - lo) / (hi - lo) * r.height()
|
|
||||||
painter.setBrush(QColor("#58a6ff"))
|
|
||||||
painter.drawEllipse(int(x - 4), int(y - 4), 8, 8)
|
|
||||||
return
|
|
||||||
|
|
||||||
points = []
|
|
||||||
count = len(numeric)
|
|
||||||
for i, value in enumerate(numeric):
|
|
||||||
x = r.left() + i * r.width() / max(1, count - 1)
|
|
||||||
y = r.bottom() - (value - lo) / (hi - lo) * r.height()
|
|
||||||
points.append((x, y))
|
|
||||||
|
|
||||||
painter.setPen(QColor("#58a6ff"))
|
|
||||||
for a, b in zip(points, points[1:]):
|
|
||||||
painter.drawLine(int(a[0]), int(a[1]), int(b[0]), int(b[1]))
|
|
||||||
|
|
||||||
painter.setBrush(QColor("#58a6ff"))
|
|
||||||
for x, y in points:
|
|
||||||
painter.drawEllipse(int(x - 3), int(y - 3), 6, 6)
|
|
||||||
|
|
||||||
|
|
||||||
class AbacusIDE(QMainWindow):
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__()
|
|
||||||
|
|
||||||
self.core = AbacusCore()
|
|
||||||
self.step = 0
|
|
||||||
self.results = []
|
|
||||||
|
|
||||||
self.setWindowTitle("Abacus IDE")
|
|
||||||
self.resize(1400, 850)
|
|
||||||
self.setMinimumSize(1050, 650)
|
|
||||||
|
|
||||||
self._build_main()
|
|
||||||
self._build_toolbar()
|
|
||||||
self._build_docks()
|
|
||||||
self._build_statusbar()
|
|
||||||
|
|
||||||
self._refresh_sidebars()
|
|
||||||
self.editor.setFocus()
|
|
||||||
|
|
||||||
def _build_toolbar(self):
|
|
||||||
toolbar = QToolBar()
|
|
||||||
toolbar.setMovable(False)
|
|
||||||
toolbar.setToolButtonStyle(Qt.ToolButtonTextOnly)
|
|
||||||
self.addToolBar(toolbar)
|
|
||||||
|
|
||||||
run = toolbar.addAction("▶ Run")
|
|
||||||
run.triggered.connect(self.run_document)
|
|
||||||
|
|
||||||
run_line = toolbar.addAction("Run line")
|
|
||||||
run_line.triggered.connect(self.run_current_line)
|
|
||||||
|
|
||||||
toolbar.addSeparator()
|
|
||||||
|
|
||||||
for symbol in ("+", "-", "*", "/", "(", ")", "[", "]", "="):
|
|
||||||
button = QPushButton(symbol)
|
|
||||||
button.setObjectName("operator")
|
|
||||||
button.clicked.connect(
|
|
||||||
lambda checked=False, s=symbol: self.insert_operator(s)
|
|
||||||
)
|
|
||||||
toolbar.addWidget(button)
|
|
||||||
|
|
||||||
toolbar.addSeparator()
|
|
||||||
|
|
||||||
clear = toolbar.addAction("Clear")
|
|
||||||
clear.triggered.connect(self.editor.clear)
|
|
||||||
|
|
||||||
toolbar.addSeparator()
|
|
||||||
|
|
||||||
reset = toolbar.addAction("Reset")
|
|
||||||
reset.triggered.connect(self.reset_abacus)
|
|
||||||
|
|
||||||
def _build_main(self):
|
|
||||||
self.tabs = QTabWidget()
|
|
||||||
self.setCentralWidget(self.tabs)
|
|
||||||
|
|
||||||
# Workspace tab
|
|
||||||
workspace = QWidget()
|
|
||||||
workspace_layout = QVBoxLayout(workspace)
|
|
||||||
workspace_layout.setContentsMargins(10, 10, 10, 10)
|
|
||||||
workspace_layout.setSpacing(8)
|
|
||||||
|
|
||||||
editor_header = QHBoxLayout()
|
|
||||||
label = QLabel("Expression editor")
|
|
||||||
label.setStyleSheet(
|
|
||||||
"font-size: 13px; font-weight: 700; color: #c9d1d9;"
|
|
||||||
)
|
|
||||||
|
|
||||||
hint = QLabel(
|
|
||||||
"One command per line • # comments • Enter = run line"
|
|
||||||
)
|
|
||||||
hint.setStyleSheet("color: #657585;")
|
|
||||||
|
|
||||||
editor_header.addWidget(label)
|
|
||||||
editor_header.addStretch()
|
|
||||||
editor_header.addWidget(hint)
|
|
||||||
workspace_layout.addLayout(editor_header)
|
|
||||||
|
|
||||||
self.editor = CodeEditor()
|
|
||||||
AbacusHighlighter(self.editor.document())
|
|
||||||
self.editor.setPlainText(
|
|
||||||
"# Example\n"
|
|
||||||
"alpha = 10\n"
|
|
||||||
"beta = 2\n"
|
|
||||||
"alpha + beta * 3\n"
|
|
||||||
)
|
|
||||||
workspace_layout.addWidget(self.editor, 1)
|
|
||||||
|
|
||||||
bottom = QFrame()
|
|
||||||
bottom.setObjectName("bottomCard")
|
|
||||||
bottom_layout = QHBoxLayout(bottom)
|
|
||||||
bottom_layout.setContentsMargins(10, 8, 10, 8)
|
|
||||||
|
|
||||||
bottom_layout.addWidget(QLabel("Quick input:"))
|
|
||||||
|
|
||||||
self.single_line = QLineEdit()
|
|
||||||
self.single_line.setObjectName("singleLine")
|
|
||||||
self.single_line.setPlaceholderText("Enter expression...")
|
|
||||||
self.single_line.returnPressed.connect(self.run_single_line)
|
|
||||||
bottom_layout.addWidget(self.single_line, 1)
|
|
||||||
|
|
||||||
execute = QPushButton("Execute")
|
|
||||||
execute.clicked.connect(self.run_single_line)
|
|
||||||
bottom_layout.addWidget(execute)
|
|
||||||
|
|
||||||
workspace_layout.addWidget(bottom)
|
|
||||||
|
|
||||||
self.tabs.addTab(workspace, "Workspace")
|
|
||||||
|
|
||||||
# Results tab
|
|
||||||
results_page = QWidget()
|
|
||||||
results_layout = QVBoxLayout(results_page)
|
|
||||||
results_layout.setContentsMargins(10, 10, 10, 10)
|
|
||||||
|
|
||||||
self.results_table = QTableWidget(0, 4)
|
|
||||||
self.results_table.setHorizontalHeaderLabels(
|
|
||||||
["Step", "Expression", "Result", "Comment"]
|
|
||||||
)
|
|
||||||
self.results_table.horizontalHeader().setStretchLastSection(True)
|
|
||||||
self.results_table.setAlternatingRowColors(False)
|
|
||||||
results_layout.addWidget(self.results_table)
|
|
||||||
|
|
||||||
self.tabs.addTab(results_page, "Results")
|
|
||||||
|
|
||||||
# Graph tab
|
|
||||||
graph_page = QWidget()
|
|
||||||
graph_layout = QVBoxLayout(graph_page)
|
|
||||||
graph_layout.setContentsMargins(10, 10, 10, 10)
|
|
||||||
|
|
||||||
graph_title = QLabel("Numeric result history")
|
|
||||||
graph_title.setStyleSheet(
|
|
||||||
"font-size: 13px; font-weight: 700; color: #c9d1d9;"
|
|
||||||
)
|
|
||||||
graph_layout.addWidget(graph_title)
|
|
||||||
|
|
||||||
self.graph = ResultGraph()
|
|
||||||
graph_layout.addWidget(self.graph, 1)
|
|
||||||
|
|
||||||
graph_info = QLabel(
|
|
||||||
"The graph uses numeric results from executed expressions."
|
|
||||||
)
|
|
||||||
graph_info.setStyleSheet("color: #657585;")
|
|
||||||
graph_layout.addWidget(graph_info)
|
|
||||||
|
|
||||||
self.tabs.addTab(graph_page, "Graph")
|
|
||||||
|
|
||||||
def _build_docks(self):
|
|
||||||
self.variables_dock = self._make_dock(
|
|
||||||
"Variables", self._make_list_widget()
|
|
||||||
)
|
|
||||||
self.functions_dock = self._make_dock(
|
|
||||||
"Functions", self._make_list_widget()
|
|
||||||
)
|
|
||||||
self.history_dock = self._make_dock(
|
|
||||||
"History", self._make_list_widget()
|
|
||||||
)
|
|
||||||
|
|
||||||
self.variables_list = self.variables_dock.widget()
|
|
||||||
self.functions_list = self.functions_dock.widget()
|
|
||||||
self.history_list = self.history_dock.widget()
|
|
||||||
|
|
||||||
self.addDockWidget(Qt.RightDockWidgetArea, self.variables_dock)
|
|
||||||
self.addDockWidget(Qt.RightDockWidgetArea, self.functions_dock)
|
|
||||||
self.addDockWidget(Qt.RightDockWidgetArea, self.history_dock)
|
|
||||||
|
|
||||||
self.tabifyDockWidget(self.variables_dock, self.functions_dock)
|
|
||||||
self.tabifyDockWidget(self.functions_dock, self.history_dock)
|
|
||||||
self.variables_dock.raise_()
|
|
||||||
|
|
||||||
def _make_list_widget(self):
|
|
||||||
return QListWidget()
|
|
||||||
|
|
||||||
def _make_dock(self, title, widget):
|
|
||||||
dock = QDockWidget(title, self)
|
|
||||||
dock.setAllowedAreas(
|
|
||||||
Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea
|
|
||||||
)
|
|
||||||
dock.setFeatures(
|
|
||||||
QDockWidget.DockWidgetMovable
|
|
||||||
| QDockWidget.DockWidgetFloatable
|
|
||||||
)
|
|
||||||
dock.setWidget(widget)
|
|
||||||
return dock
|
|
||||||
|
|
||||||
def _build_statusbar(self):
|
|
||||||
self.status = QStatusBar()
|
|
||||||
self.setStatusBar(self.status)
|
|
||||||
self.status.showMessage("Ready")
|
|
||||||
|
|
||||||
def insert_operator(self, symbol):
|
|
||||||
self.editor.insertPlainText(symbol)
|
|
||||||
self.editor.setFocus()
|
|
||||||
|
|
||||||
def run_single_line(self):
|
|
||||||
text = self.single_line.text().strip()
|
|
||||||
if not text:
|
|
||||||
return
|
|
||||||
|
|
||||||
self._execute(text)
|
|
||||||
self.single_line.clear()
|
|
||||||
|
|
||||||
def run_current_line(self):
|
|
||||||
cursor = self.editor.textCursor()
|
|
||||||
cursor.select(cursor.LineUnderCursor)
|
|
||||||
line = cursor.selectedText().strip()
|
|
||||||
|
|
||||||
if line:
|
|
||||||
self._execute(line)
|
|
||||||
|
|
||||||
def run_document(self):
|
|
||||||
lines = self.editor.toPlainText().splitlines()
|
|
||||||
|
|
||||||
executed = 0
|
|
||||||
for line in lines:
|
|
||||||
line = line.strip()
|
|
||||||
if not line or line.startswith("#"):
|
|
||||||
continue
|
|
||||||
|
|
||||||
self._execute(line)
|
|
||||||
executed += 1
|
|
||||||
|
|
||||||
self.status.showMessage(f"Executed {executed} command(s)")
|
|
||||||
|
|
||||||
def _execute(self, text):
|
|
||||||
self.step += 1
|
|
||||||
|
|
||||||
try:
|
|
||||||
result = self.core.parse_input(text)
|
|
||||||
except Exception as exc:
|
|
||||||
self.step -= 1
|
|
||||||
self.status.showMessage(f"Error: {exc}")
|
|
||||||
self.history_list.addItem(f"ERROR {text} → {exc}")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Critical: assignments/functions return None, but their
|
|
||||||
# dictionaries are already changed. Refresh immediately.
|
|
||||||
self._refresh_sidebars()
|
|
||||||
|
|
||||||
if not result:
|
|
||||||
self.status.showMessage(f"Updated: {text}")
|
|
||||||
return
|
|
||||||
|
|
||||||
original, _, value, comment = result
|
|
||||||
value_text = str(value)
|
|
||||||
comment_text = comment or ""
|
|
||||||
|
|
||||||
self.results.append(
|
|
||||||
{
|
|
||||||
"step": self.step,
|
|
||||||
"expression": original,
|
|
||||||
"result": value_text,
|
|
||||||
"comment": comment_text,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.history_list.addItem(
|
|
||||||
f"{self.step}. {original} = {value_text}"
|
|
||||||
)
|
|
||||||
self.history_list.scrollToBottom()
|
|
||||||
|
|
||||||
self._add_result_row(
|
|
||||||
self.step, original, value_text, comment_text
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
numeric = float(value)
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
numeric = None
|
|
||||||
|
|
||||||
if numeric is not None and math.isfinite(numeric):
|
|
||||||
self.graph.values.append(numeric)
|
|
||||||
self.graph.set_values(self.graph.values)
|
|
||||||
|
|
||||||
self.status.showMessage(f"Result: {value_text}")
|
|
||||||
|
|
||||||
def _add_result_row(self, step, expression, result, comment):
|
|
||||||
row = self.results_table.rowCount()
|
|
||||||
self.results_table.insertRow(row)
|
|
||||||
|
|
||||||
values = [str(step), expression, result, comment]
|
|
||||||
|
|
||||||
for col, value in enumerate(values):
|
|
||||||
self.results_table.setItem(
|
|
||||||
row, col, QTableWidgetItem(value)
|
|
||||||
)
|
|
||||||
|
|
||||||
self.results_table.resizeColumnsToContents()
|
|
||||||
self.results_table.scrollToBottom()
|
|
||||||
|
|
||||||
def _refresh_sidebars(self):
|
|
||||||
self.variables_list.clear()
|
|
||||||
|
|
||||||
for key, value in self.core.get_vars().items():
|
|
||||||
self.variables_list.addItem(f"{key} = {value}")
|
|
||||||
|
|
||||||
self.functions_list.clear()
|
|
||||||
|
|
||||||
for key, value in self.core.get_funcs().items():
|
|
||||||
self.functions_list.addItem(f"{key} = {value}")
|
|
||||||
|
|
||||||
def reset_abacus(self):
|
|
||||||
self.core = AbacusCore()
|
|
||||||
self.step = 0
|
|
||||||
self.results.clear()
|
|
||||||
|
|
||||||
self.variables_list.clear()
|
|
||||||
self.functions_list.clear()
|
|
||||||
self.history_list.clear()
|
|
||||||
self.results_table.setRowCount(0)
|
|
||||||
|
|
||||||
self.graph.set_values([])
|
|
||||||
self.status.showMessage("Abacus state reset")
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
app = QApplication(sys.argv)
|
|
||||||
app.setStyle("Fusion")
|
|
||||||
app.setStyleSheet(STYLE)
|
|
||||||
|
|
||||||
window = AbacusIDE()
|
|
||||||
window.show()
|
|
||||||
|
|
||||||
sys.exit(app.exec())
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
|
|
@ -1,764 +0,0 @@
|
||||||
import math
|
|
||||||
import sys
|
|
||||||
|
|
||||||
from PySide6.QtCore import Qt, QRect, QSize
|
|
||||||
from PySide6.QtGui import (
|
|
||||||
QColor,
|
|
||||||
QFont,
|
|
||||||
QPainter,
|
|
||||||
QSyntaxHighlighter,
|
|
||||||
QTextCharFormat,
|
|
||||||
)
|
|
||||||
from PySide6.QtWidgets import (
|
|
||||||
QApplication,
|
|
||||||
QDockWidget,
|
|
||||||
QFrame,
|
|
||||||
QHBoxLayout,
|
|
||||||
QLabel,
|
|
||||||
QLineEdit,
|
|
||||||
QListWidget,
|
|
||||||
QMainWindow,
|
|
||||||
QPlainTextEdit,
|
|
||||||
QPushButton,
|
|
||||||
QSplitter,
|
|
||||||
QStatusBar,
|
|
||||||
QTabWidget,
|
|
||||||
QTableWidget,
|
|
||||||
QTableWidgetItem,
|
|
||||||
QToolBar,
|
|
||||||
QVBoxLayout,
|
|
||||||
QWidget,
|
|
||||||
)
|
|
||||||
|
|
||||||
from abacus_core import AbacusCore
|
|
||||||
|
|
||||||
|
|
||||||
STYLE = """
|
|
||||||
QMainWindow, QWidget {
|
|
||||||
background: #0d1117;
|
|
||||||
color: #e6edf3;
|
|
||||||
font-family: "Segoe UI";
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
QToolBar {
|
|
||||||
background: #111820;
|
|
||||||
border: 0;
|
|
||||||
border-bottom: 1px solid #202a36;
|
|
||||||
spacing: 5px;
|
|
||||||
padding: 6px 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
QToolButton {
|
|
||||||
color: #c9d1d9;
|
|
||||||
background: transparent;
|
|
||||||
border-radius: 7px;
|
|
||||||
padding: 7px 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
QToolButton:hover {
|
|
||||||
background: #1b2531;
|
|
||||||
color: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
QTabWidget::pane {
|
|
||||||
border: 1px solid #202a36;
|
|
||||||
background: #0d1117;
|
|
||||||
}
|
|
||||||
|
|
||||||
QTabBar::tab {
|
|
||||||
background: #111820;
|
|
||||||
color: #7d8b99;
|
|
||||||
border: 1px solid #202a36;
|
|
||||||
border-bottom: none;
|
|
||||||
padding: 9px 16px;
|
|
||||||
margin-right: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
QTabBar::tab:selected {
|
|
||||||
background: #17212c;
|
|
||||||
color: #f0f6fc;
|
|
||||||
}
|
|
||||||
|
|
||||||
QDockWidget {
|
|
||||||
color: #aeb9c6;
|
|
||||||
titlebar-close-icon: none;
|
|
||||||
titlebar-normal-icon: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
QDockWidget::title {
|
|
||||||
background: #111820;
|
|
||||||
border-bottom: 1px solid #202a36;
|
|
||||||
padding: 9px 11px;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
QFrame#editorCard, QFrame#bottomCard {
|
|
||||||
background: #111820;
|
|
||||||
border: 1px solid #202a36;
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
QPlainTextEdit {
|
|
||||||
background: #0b0f14;
|
|
||||||
color: #d6deea;
|
|
||||||
border: 1px solid #202a36;
|
|
||||||
border-radius: 9px;
|
|
||||||
selection-background-color: #244a7d;
|
|
||||||
padding: 8px;
|
|
||||||
font-family: "JetBrains Mono", "Consolas", monospace;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
QLineEdit#singleLine {
|
|
||||||
background: #0b0f14;
|
|
||||||
color: #f0f6fc;
|
|
||||||
border: 1px solid #303b48;
|
|
||||||
border-radius: 9px;
|
|
||||||
padding: 9px 11px;
|
|
||||||
font-family: "JetBrains Mono", "Consolas", monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
QLineEdit#singleLine:focus {
|
|
||||||
border: 1px solid #3b82f6;
|
|
||||||
}
|
|
||||||
|
|
||||||
QPushButton {
|
|
||||||
background: #2563eb;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 8px 13px;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
QPushButton:hover {
|
|
||||||
background: #3475ef;
|
|
||||||
}
|
|
||||||
|
|
||||||
QPushButton#operator {
|
|
||||||
background: #1b2531;
|
|
||||||
color: #cbd5e1;
|
|
||||||
border: 1px solid #293543;
|
|
||||||
min-width: 34px;
|
|
||||||
}
|
|
||||||
|
|
||||||
QPushButton#operator:hover {
|
|
||||||
background: #253344;
|
|
||||||
color: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
QPushButton#secondary {
|
|
||||||
background: #1b2531;
|
|
||||||
color: #b9c4d0;
|
|
||||||
border: 1px solid #293543;
|
|
||||||
}
|
|
||||||
|
|
||||||
QListWidget, QTableWidget {
|
|
||||||
background: #0d131a;
|
|
||||||
color: #cbd5e1;
|
|
||||||
border: 1px solid #202a36;
|
|
||||||
border-radius: 8px;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
QListWidget::item {
|
|
||||||
padding: 7px 8px;
|
|
||||||
border-radius: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
QListWidget::item:hover {
|
|
||||||
background: #17212c;
|
|
||||||
}
|
|
||||||
|
|
||||||
QListWidget::item:selected {
|
|
||||||
background: #1c3153;
|
|
||||||
color: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
QHeaderView::section {
|
|
||||||
background: #161f29;
|
|
||||||
color: #9aa8b7;
|
|
||||||
border: none;
|
|
||||||
border-right: 1px solid #202a36;
|
|
||||||
border-bottom: 1px solid #202a36;
|
|
||||||
padding: 7px;
|
|
||||||
}
|
|
||||||
|
|
||||||
QTableWidget::item {
|
|
||||||
padding: 6px;
|
|
||||||
border-bottom: 1px solid #18212b;
|
|
||||||
}
|
|
||||||
|
|
||||||
QStatusBar {
|
|
||||||
background: #0b0f14;
|
|
||||||
color: #7d8b99;
|
|
||||||
border-top: 1px solid #202a36;
|
|
||||||
}
|
|
||||||
|
|
||||||
QScrollBar:vertical {
|
|
||||||
background: #0d131a;
|
|
||||||
width: 9px;
|
|
||||||
}
|
|
||||||
|
|
||||||
QScrollBar::handle:vertical {
|
|
||||||
background: #303c4a;
|
|
||||||
border-radius: 4px;
|
|
||||||
min-height: 25px;
|
|
||||||
}
|
|
||||||
|
|
||||||
QScrollBar::add-line:vertical,
|
|
||||||
QScrollBar::sub-line:vertical {
|
|
||||||
height: 0;
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class AbacusHighlighter(QSyntaxHighlighter):
|
|
||||||
def __init__(self, document):
|
|
||||||
super().__init__(document)
|
|
||||||
|
|
||||||
self.number = QTextCharFormat()
|
|
||||||
self.number.setForeground(QColor("#79c0ff"))
|
|
||||||
|
|
||||||
self.operator = QTextCharFormat()
|
|
||||||
self.operator.setForeground(QColor("#ff7b72"))
|
|
||||||
|
|
||||||
self.comment = QTextCharFormat()
|
|
||||||
self.comment.setForeground(QColor("#6e7681"))
|
|
||||||
self.comment.setFontItalic(True)
|
|
||||||
|
|
||||||
self.assignment = QTextCharFormat()
|
|
||||||
self.assignment.setForeground(QColor("#d2a8ff"))
|
|
||||||
|
|
||||||
self.keyword = QTextCharFormat()
|
|
||||||
self.keyword.setForeground(QColor("#ffa657"))
|
|
||||||
|
|
||||||
def highlightBlock(self, text):
|
|
||||||
# Comments
|
|
||||||
comment_pos = text.find("#")
|
|
||||||
if comment_pos >= 0:
|
|
||||||
self.setFormat(comment_pos, len(text) - comment_pos, self.comment)
|
|
||||||
code = text[:comment_pos]
|
|
||||||
else:
|
|
||||||
code = text
|
|
||||||
|
|
||||||
# Numbers
|
|
||||||
i = 0
|
|
||||||
while i < len(code):
|
|
||||||
if code[i].isdigit() or (
|
|
||||||
code[i] == "." and i + 1 < len(code) and code[i + 1].isdigit()
|
|
||||||
):
|
|
||||||
start = i
|
|
||||||
i += 1
|
|
||||||
while i < len(code) and (code[i].isdigit() or code[i] == "."):
|
|
||||||
i += 1
|
|
||||||
self.setFormat(start, i - start, self.number)
|
|
||||||
else:
|
|
||||||
i += 1
|
|
||||||
|
|
||||||
for char in "+-*/=():[];":
|
|
||||||
start = 0
|
|
||||||
while True:
|
|
||||||
pos = code.find(char, start)
|
|
||||||
if pos < 0:
|
|
||||||
break
|
|
||||||
self.setFormat(pos, 1, self.operator)
|
|
||||||
start = pos + 1
|
|
||||||
|
|
||||||
# Function definitions: name before ':'
|
|
||||||
colon = code.find(":")
|
|
||||||
if colon > 0:
|
|
||||||
name = code[:colon].strip()
|
|
||||||
if name:
|
|
||||||
pos = code.find(name)
|
|
||||||
self.setFormat(pos, len(name), self.keyword)
|
|
||||||
|
|
||||||
|
|
||||||
class LineNumberArea(QWidget):
|
|
||||||
def __init__(self, editor):
|
|
||||||
super().__init__(editor)
|
|
||||||
self.editor = editor
|
|
||||||
|
|
||||||
def sizeHint(self):
|
|
||||||
return QSize(self.editor.line_number_width(), 0)
|
|
||||||
|
|
||||||
def paintEvent(self, event):
|
|
||||||
painter = QPainter(self)
|
|
||||||
painter.fillRect(event.rect(), QColor("#0f151c"))
|
|
||||||
|
|
||||||
block = self.editor.firstVisibleBlock()
|
|
||||||
block_number = block.blockNumber()
|
|
||||||
top = int(
|
|
||||||
self.editor.blockBoundingGeometry(block).translated(
|
|
||||||
self.editor.contentOffset()
|
|
||||||
).top()
|
|
||||||
)
|
|
||||||
bottom = top + int(self.editor.blockBoundingRect(block).height())
|
|
||||||
|
|
||||||
while block.isValid() and top <= event.rect().bottom():
|
|
||||||
if block.isVisible() and bottom >= event.rect().top():
|
|
||||||
painter.setPen(QColor("#596878"))
|
|
||||||
painter.drawText(
|
|
||||||
0,
|
|
||||||
top,
|
|
||||||
self.width() - 7,
|
|
||||||
self.editor.fontMetrics().height(),
|
|
||||||
Qt.AlignRight,
|
|
||||||
str(block_number + 1),
|
|
||||||
)
|
|
||||||
|
|
||||||
block = block.next()
|
|
||||||
top = bottom
|
|
||||||
bottom = top + int(self.editor.blockBoundingRect(block).height())
|
|
||||||
block_number += 1
|
|
||||||
|
|
||||||
|
|
||||||
class CodeEditor(QPlainTextEdit):
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__()
|
|
||||||
|
|
||||||
self.line_numbers = LineNumberArea(self)
|
|
||||||
self.blockCountChanged.connect(self.update_line_number_width)
|
|
||||||
self.updateRequest.connect(self.update_line_number_area)
|
|
||||||
self.cursorPositionChanged.connect(self.highlight_current_line)
|
|
||||||
|
|
||||||
self.update_line_number_width(0)
|
|
||||||
self.highlight_current_line()
|
|
||||||
|
|
||||||
font = QFont("JetBrains Mono", 11)
|
|
||||||
font.setStyleHint(QFont.Monospace)
|
|
||||||
self.setFont(font)
|
|
||||||
self.setTabStopDistance(
|
|
||||||
self.fontMetrics().horizontalAdvance(" ") * 4
|
|
||||||
)
|
|
||||||
|
|
||||||
def line_number_width(self):
|
|
||||||
digits = len(str(max(1, self.blockCount())))
|
|
||||||
return 12 + self.fontMetrics().horizontalAdvance("9") * digits
|
|
||||||
|
|
||||||
def update_line_number_width(self, _):
|
|
||||||
self.setViewportMargins(self.line_number_width(), 0, 0, 0)
|
|
||||||
|
|
||||||
def update_line_number_area(self, rect, dy):
|
|
||||||
if dy:
|
|
||||||
self.line_numbers.scroll(0, dy)
|
|
||||||
else:
|
|
||||||
self.line_numbers.update(0, rect.y(), self.line_numbers.width(), rect.height())
|
|
||||||
|
|
||||||
if rect.contains(self.viewport().rect()):
|
|
||||||
self.update_line_number_width(0)
|
|
||||||
|
|
||||||
def resizeEvent(self, event):
|
|
||||||
super().resizeEvent(event)
|
|
||||||
rect = self.contentsRect()
|
|
||||||
rect.setWidth(self.line_number_width())
|
|
||||||
self.line_numbers.setGeometry(rect)
|
|
||||||
|
|
||||||
def highlight_current_line(self):
|
|
||||||
extra = []
|
|
||||||
if not self.isReadOnly():
|
|
||||||
selection = QTextCharFormat()
|
|
||||||
selection.setBackground(QColor("#111a24"))
|
|
||||||
selection.setProperty(QTextCharFormat.FullWidthSelection, True)
|
|
||||||
extra_selection = self.ExtraSelection()
|
|
||||||
extra_selection.cursor = self.textCursor()
|
|
||||||
extra_selection.format = selection
|
|
||||||
extra.append(extra_selection)
|
|
||||||
self.setExtraSelections(extra)
|
|
||||||
|
|
||||||
|
|
||||||
class ResultGraph(QWidget):
|
|
||||||
"""Small dependency-free graph of numeric results."""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__()
|
|
||||||
self.values = []
|
|
||||||
|
|
||||||
def set_values(self, values):
|
|
||||||
self.values = values[-50:]
|
|
||||||
self.update()
|
|
||||||
|
|
||||||
def paintEvent(self, event):
|
|
||||||
painter = QPainter(self)
|
|
||||||
painter.setRenderHint(QPainter.Antialiasing)
|
|
||||||
painter.fillRect(self.rect(), QColor("#0b0f14"))
|
|
||||||
|
|
||||||
r = self.rect().adjusted(45, 20, -20, -35)
|
|
||||||
painter.setPen(QColor("#273442"))
|
|
||||||
painter.drawRect(r)
|
|
||||||
|
|
||||||
if not self.values:
|
|
||||||
painter.setPen(QColor("#647383"))
|
|
||||||
painter.drawText(r, Qt.AlignCenter, "No numeric results yet")
|
|
||||||
return
|
|
||||||
|
|
||||||
numeric = [v for v in self.values if math.isfinite(v)]
|
|
||||||
if not numeric:
|
|
||||||
return
|
|
||||||
|
|
||||||
lo = min(numeric)
|
|
||||||
hi = max(numeric)
|
|
||||||
if abs(hi - lo) < 1e-12:
|
|
||||||
lo -= 1
|
|
||||||
hi += 1
|
|
||||||
|
|
||||||
painter.setPen(QColor("#344454"))
|
|
||||||
for frac in (0.0, 0.5, 1.0):
|
|
||||||
y = int(r.bottom() - frac * r.height())
|
|
||||||
painter.drawLine(r.left(), y, r.right(), y)
|
|
||||||
|
|
||||||
painter.setPen(QColor("#5f6f80"))
|
|
||||||
painter.drawText(5, r.top() + 5, 35, 20, Qt.AlignRight, f"{hi:g}")
|
|
||||||
painter.drawText(5, r.bottom() - 15, 35, 20, Qt.AlignRight, f"{lo:g}")
|
|
||||||
|
|
||||||
if len(numeric) == 1:
|
|
||||||
x = r.center().x()
|
|
||||||
y = r.bottom() - (numeric[0] - lo) / (hi - lo) * r.height()
|
|
||||||
painter.setBrush(QColor("#58a6ff"))
|
|
||||||
painter.drawEllipse(int(x - 4), int(y - 4), 8, 8)
|
|
||||||
return
|
|
||||||
|
|
||||||
points = []
|
|
||||||
count = len(numeric)
|
|
||||||
for i, value in enumerate(numeric):
|
|
||||||
x = r.left() + i * r.width() / max(1, count - 1)
|
|
||||||
y = r.bottom() - (value - lo) / (hi - lo) * r.height()
|
|
||||||
points.append((x, y))
|
|
||||||
|
|
||||||
painter.setPen(QColor("#58a6ff"))
|
|
||||||
for a, b in zip(points, points[1:]):
|
|
||||||
painter.drawLine(int(a[0]), int(a[1]), int(b[0]), int(b[1]))
|
|
||||||
|
|
||||||
painter.setBrush(QColor("#58a6ff"))
|
|
||||||
for x, y in points:
|
|
||||||
painter.drawEllipse(int(x - 3), int(y - 3), 6, 6)
|
|
||||||
|
|
||||||
|
|
||||||
class AbacusIDE(QMainWindow):
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__()
|
|
||||||
|
|
||||||
self.core = AbacusCore()
|
|
||||||
self.step = 0
|
|
||||||
self.results = []
|
|
||||||
|
|
||||||
self.setWindowTitle("Abacus IDE")
|
|
||||||
self.resize(1400, 850)
|
|
||||||
self.setMinimumSize(1050, 650)
|
|
||||||
|
|
||||||
self._build_main()
|
|
||||||
self._build_toolbar()
|
|
||||||
self._build_docks()
|
|
||||||
self._build_statusbar()
|
|
||||||
|
|
||||||
self._refresh_sidebars()
|
|
||||||
self.editor.setFocus()
|
|
||||||
|
|
||||||
def _build_toolbar(self):
|
|
||||||
toolbar = QToolBar()
|
|
||||||
toolbar.setMovable(False)
|
|
||||||
toolbar.setToolButtonStyle(Qt.ToolButtonTextOnly)
|
|
||||||
self.addToolBar(toolbar)
|
|
||||||
|
|
||||||
run = toolbar.addAction("▶ Run")
|
|
||||||
run.triggered.connect(self.run_document)
|
|
||||||
|
|
||||||
run_line = toolbar.addAction("Run line")
|
|
||||||
run_line.triggered.connect(self.run_current_line)
|
|
||||||
|
|
||||||
toolbar.addSeparator()
|
|
||||||
|
|
||||||
for symbol in ("+", "-", "*", "/", "(", ")", "[", "]", "="):
|
|
||||||
button = QPushButton(symbol)
|
|
||||||
button.setObjectName("operator")
|
|
||||||
button.clicked.connect(
|
|
||||||
lambda checked=False, s=symbol: self.insert_operator(s)
|
|
||||||
)
|
|
||||||
toolbar.addWidget(button)
|
|
||||||
|
|
||||||
toolbar.addSeparator()
|
|
||||||
|
|
||||||
clear = toolbar.addAction("Clear")
|
|
||||||
clear.triggered.connect(self.editor.clear)
|
|
||||||
|
|
||||||
toolbar.addSeparator()
|
|
||||||
|
|
||||||
reset = toolbar.addAction("Reset")
|
|
||||||
reset.triggered.connect(self.reset_abacus)
|
|
||||||
|
|
||||||
def _build_main(self):
|
|
||||||
self.tabs = QTabWidget()
|
|
||||||
self.setCentralWidget(self.tabs)
|
|
||||||
|
|
||||||
# Workspace tab
|
|
||||||
workspace = QWidget()
|
|
||||||
workspace_layout = QVBoxLayout(workspace)
|
|
||||||
workspace_layout.setContentsMargins(10, 10, 10, 10)
|
|
||||||
workspace_layout.setSpacing(8)
|
|
||||||
|
|
||||||
editor_header = QHBoxLayout()
|
|
||||||
label = QLabel("Expression editor")
|
|
||||||
label.setStyleSheet(
|
|
||||||
"font-size: 13px; font-weight: 700; color: #c9d1d9;"
|
|
||||||
)
|
|
||||||
|
|
||||||
hint = QLabel(
|
|
||||||
"One command per line • # comments • Enter = run line"
|
|
||||||
)
|
|
||||||
hint.setStyleSheet("color: #657585;")
|
|
||||||
|
|
||||||
editor_header.addWidget(label)
|
|
||||||
editor_header.addStretch()
|
|
||||||
editor_header.addWidget(hint)
|
|
||||||
workspace_layout.addLayout(editor_header)
|
|
||||||
|
|
||||||
self.editor = CodeEditor()
|
|
||||||
AbacusHighlighter(self.editor.document())
|
|
||||||
self.editor.setPlainText(
|
|
||||||
"# Example\n"
|
|
||||||
"alpha = 10\n"
|
|
||||||
"beta = 2\n"
|
|
||||||
"alpha + beta * 3\n"
|
|
||||||
)
|
|
||||||
workspace_layout.addWidget(self.editor, 1)
|
|
||||||
|
|
||||||
bottom = QFrame()
|
|
||||||
bottom.setObjectName("bottomCard")
|
|
||||||
bottom_layout = QHBoxLayout(bottom)
|
|
||||||
bottom_layout.setContentsMargins(10, 8, 10, 8)
|
|
||||||
|
|
||||||
bottom_layout.addWidget(QLabel("Quick input:"))
|
|
||||||
|
|
||||||
self.single_line = QLineEdit()
|
|
||||||
self.single_line.setObjectName("singleLine")
|
|
||||||
self.single_line.setPlaceholderText("Enter expression...")
|
|
||||||
self.single_line.returnPressed.connect(self.run_single_line)
|
|
||||||
bottom_layout.addWidget(self.single_line, 1)
|
|
||||||
|
|
||||||
execute = QPushButton("Execute")
|
|
||||||
execute.clicked.connect(self.run_single_line)
|
|
||||||
bottom_layout.addWidget(execute)
|
|
||||||
|
|
||||||
workspace_layout.addWidget(bottom)
|
|
||||||
|
|
||||||
self.tabs.addTab(workspace, "Workspace")
|
|
||||||
|
|
||||||
# Results tab
|
|
||||||
results_page = QWidget()
|
|
||||||
results_layout = QVBoxLayout(results_page)
|
|
||||||
results_layout.setContentsMargins(10, 10, 10, 10)
|
|
||||||
|
|
||||||
self.results_table = QTableWidget(0, 4)
|
|
||||||
self.results_table.setHorizontalHeaderLabels(
|
|
||||||
["Step", "Expression", "Result", "Comment"]
|
|
||||||
)
|
|
||||||
self.results_table.horizontalHeader().setStretchLastSection(True)
|
|
||||||
self.results_table.setAlternatingRowColors(False)
|
|
||||||
results_layout.addWidget(self.results_table)
|
|
||||||
|
|
||||||
self.tabs.addTab(results_page, "Results")
|
|
||||||
|
|
||||||
# Graph tab
|
|
||||||
graph_page = QWidget()
|
|
||||||
graph_layout = QVBoxLayout(graph_page)
|
|
||||||
graph_layout.setContentsMargins(10, 10, 10, 10)
|
|
||||||
|
|
||||||
graph_title = QLabel("Numeric result history")
|
|
||||||
graph_title.setStyleSheet(
|
|
||||||
"font-size: 13px; font-weight: 700; color: #c9d1d9;"
|
|
||||||
)
|
|
||||||
graph_layout.addWidget(graph_title)
|
|
||||||
|
|
||||||
self.graph = ResultGraph()
|
|
||||||
graph_layout.addWidget(self.graph, 1)
|
|
||||||
|
|
||||||
graph_info = QLabel(
|
|
||||||
"The graph uses numeric results from executed expressions."
|
|
||||||
)
|
|
||||||
graph_info.setStyleSheet("color: #657585;")
|
|
||||||
graph_layout.addWidget(graph_info)
|
|
||||||
|
|
||||||
self.tabs.addTab(graph_page, "Graph")
|
|
||||||
|
|
||||||
def _build_docks(self):
|
|
||||||
self.variables_dock = self._make_dock(
|
|
||||||
"Variables", self._make_list_widget()
|
|
||||||
)
|
|
||||||
self.functions_dock = self._make_dock(
|
|
||||||
"Functions", self._make_list_widget()
|
|
||||||
)
|
|
||||||
self.history_dock = self._make_dock(
|
|
||||||
"History", self._make_list_widget()
|
|
||||||
)
|
|
||||||
|
|
||||||
self.variables_list = self.variables_dock.widget()
|
|
||||||
self.functions_list = self.functions_dock.widget()
|
|
||||||
self.history_list = self.history_dock.widget()
|
|
||||||
|
|
||||||
self.addDockWidget(Qt.RightDockWidgetArea, self.variables_dock)
|
|
||||||
self.addDockWidget(Qt.RightDockWidgetArea, self.functions_dock)
|
|
||||||
self.addDockWidget(Qt.RightDockWidgetArea, self.history_dock)
|
|
||||||
|
|
||||||
self.tabifyDockWidget(self.variables_dock, self.functions_dock)
|
|
||||||
self.tabifyDockWidget(self.functions_dock, self.history_dock)
|
|
||||||
self.variables_dock.raise_()
|
|
||||||
|
|
||||||
def _make_list_widget(self):
|
|
||||||
return QListWidget()
|
|
||||||
|
|
||||||
def _make_dock(self, title, widget):
|
|
||||||
dock = QDockWidget(title, self)
|
|
||||||
dock.setAllowedAreas(
|
|
||||||
Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea
|
|
||||||
)
|
|
||||||
dock.setFeatures(
|
|
||||||
QDockWidget.DockWidgetMovable
|
|
||||||
| QDockWidget.DockWidgetFloatable
|
|
||||||
)
|
|
||||||
dock.setWidget(widget)
|
|
||||||
return dock
|
|
||||||
|
|
||||||
def _build_statusbar(self):
|
|
||||||
self.status = QStatusBar()
|
|
||||||
self.setStatusBar(self.status)
|
|
||||||
self.status.showMessage("Ready")
|
|
||||||
|
|
||||||
def insert_operator(self, symbol):
|
|
||||||
self.editor.insertPlainText(symbol)
|
|
||||||
self.editor.setFocus()
|
|
||||||
|
|
||||||
def run_single_line(self):
|
|
||||||
text = self.single_line.text().strip()
|
|
||||||
if not text:
|
|
||||||
return
|
|
||||||
|
|
||||||
self._execute(text)
|
|
||||||
self.single_line.clear()
|
|
||||||
|
|
||||||
def run_current_line(self):
|
|
||||||
cursor = self.editor.textCursor()
|
|
||||||
cursor.select(cursor.LineUnderCursor)
|
|
||||||
line = cursor.selectedText().strip()
|
|
||||||
|
|
||||||
if line:
|
|
||||||
self._execute(line)
|
|
||||||
|
|
||||||
def run_document(self):
|
|
||||||
lines = self.editor.toPlainText().splitlines()
|
|
||||||
|
|
||||||
executed = 0
|
|
||||||
for line in lines:
|
|
||||||
line = line.strip()
|
|
||||||
if not line or line.startswith("#"):
|
|
||||||
continue
|
|
||||||
|
|
||||||
self._execute(line)
|
|
||||||
executed += 1
|
|
||||||
|
|
||||||
self.status.showMessage(f"Executed {executed} command(s)")
|
|
||||||
|
|
||||||
def _execute(self, text):
|
|
||||||
self.step += 1
|
|
||||||
|
|
||||||
try:
|
|
||||||
result = self.core.parse_input(text)
|
|
||||||
except Exception as exc:
|
|
||||||
self.step -= 1
|
|
||||||
self.status.showMessage(f"Error: {exc}")
|
|
||||||
self.history_list.addItem(f"ERROR {text} → {exc}")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Critical: assignments/functions return None, but their
|
|
||||||
# dictionaries are already changed. Refresh immediately.
|
|
||||||
self._refresh_sidebars()
|
|
||||||
|
|
||||||
if not result:
|
|
||||||
self.status.showMessage(f"Updated: {text}")
|
|
||||||
return
|
|
||||||
|
|
||||||
original, _, value, comment = result
|
|
||||||
value_text = str(value)
|
|
||||||
comment_text = comment or ""
|
|
||||||
|
|
||||||
self.results.append(
|
|
||||||
{
|
|
||||||
"step": self.step,
|
|
||||||
"expression": original,
|
|
||||||
"result": value_text,
|
|
||||||
"comment": comment_text,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.history_list.addItem(
|
|
||||||
f"{self.step}. {original} = {value_text}"
|
|
||||||
)
|
|
||||||
self.history_list.scrollToBottom()
|
|
||||||
|
|
||||||
self._add_result_row(
|
|
||||||
self.step, original, value_text, comment_text
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
numeric = float(value)
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
numeric = None
|
|
||||||
|
|
||||||
if numeric is not None and math.isfinite(numeric):
|
|
||||||
self.graph.values.append(numeric)
|
|
||||||
self.graph.set_values(self.graph.values)
|
|
||||||
|
|
||||||
self.status.showMessage(f"Result: {value_text}")
|
|
||||||
|
|
||||||
def _add_result_row(self, step, expression, result, comment):
|
|
||||||
row = self.results_table.rowCount()
|
|
||||||
self.results_table.insertRow(row)
|
|
||||||
|
|
||||||
values = [str(step), expression, result, comment]
|
|
||||||
|
|
||||||
for col, value in enumerate(values):
|
|
||||||
self.results_table.setItem(
|
|
||||||
row, col, QTableWidgetItem(value)
|
|
||||||
)
|
|
||||||
|
|
||||||
self.results_table.resizeColumnsToContents()
|
|
||||||
self.results_table.scrollToBottom()
|
|
||||||
|
|
||||||
def _refresh_sidebars(self):
|
|
||||||
self.variables_list.clear()
|
|
||||||
|
|
||||||
for key, value in self.core.get_vars().items():
|
|
||||||
self.variables_list.addItem(f"{key} = {value}")
|
|
||||||
|
|
||||||
self.functions_list.clear()
|
|
||||||
|
|
||||||
for key, value in self.core.get_funcs().items():
|
|
||||||
self.functions_list.addItem(f"{key} = {value}")
|
|
||||||
|
|
||||||
def reset_abacus(self):
|
|
||||||
self.core = AbacusCore()
|
|
||||||
self.step = 0
|
|
||||||
self.results.clear()
|
|
||||||
|
|
||||||
self.variables_list.clear()
|
|
||||||
self.functions_list.clear()
|
|
||||||
self.history_list.clear()
|
|
||||||
self.results_table.setRowCount(0)
|
|
||||||
|
|
||||||
self.graph.set_values([])
|
|
||||||
self.status.showMessage("Abacus state reset")
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
app = QApplication(sys.argv)
|
|
||||||
app.setStyle("Fusion")
|
|
||||||
app.setStyleSheet(STYLE)
|
|
||||||
|
|
||||||
window = AbacusIDE()
|
|
||||||
window.show()
|
|
||||||
|
|
||||||
sys.exit(app.exec())
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,354 +0,0 @@
|
||||||
import sys
|
|
||||||
|
|
||||||
from PySide6.QtCore import Qt
|
|
||||||
from PySide6.QtGui import QFont
|
|
||||||
from PySide6.QtWidgets import (
|
|
||||||
QApplication,
|
|
||||||
QFrame,
|
|
||||||
QHBoxLayout,
|
|
||||||
QLabel,
|
|
||||||
QLineEdit,
|
|
||||||
QListWidget,
|
|
||||||
QMainWindow,
|
|
||||||
QPushButton,
|
|
||||||
QSplitter,
|
|
||||||
QVBoxLayout,
|
|
||||||
QWidget,
|
|
||||||
)
|
|
||||||
|
|
||||||
from abacus_core import AbacusCore
|
|
||||||
|
|
||||||
|
|
||||||
STYLE = """
|
|
||||||
QMainWindow, QWidget {
|
|
||||||
background: #111318;
|
|
||||||
color: #e7e9ee;
|
|
||||||
font-family: "Segoe UI";
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
QFrame#topbar {
|
|
||||||
background: #171a21;
|
|
||||||
border-bottom: 1px solid #292e38;
|
|
||||||
}
|
|
||||||
|
|
||||||
QLabel#title {
|
|
||||||
font-size: 20px;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #f5f7fb;
|
|
||||||
}
|
|
||||||
|
|
||||||
QLabel#subtitle {
|
|
||||||
color: #858c99;
|
|
||||||
}
|
|
||||||
|
|
||||||
QFrame#panel {
|
|
||||||
background: #171a21;
|
|
||||||
border: 1px solid #292e38;
|
|
||||||
border-radius: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
QLabel#panelTitle {
|
|
||||||
color: #9da5b3;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
QLineEdit#input {
|
|
||||||
background: #0d0f13;
|
|
||||||
border: 1px solid #343a46;
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 12px 14px;
|
|
||||||
color: #ffffff;
|
|
||||||
font-size: 16px;
|
|
||||||
selection-background-color: #3b82f6;
|
|
||||||
}
|
|
||||||
|
|
||||||
QLineEdit#input:focus {
|
|
||||||
border: 1px solid #4b8cff;
|
|
||||||
}
|
|
||||||
|
|
||||||
QPushButton {
|
|
||||||
background: #2864d7;
|
|
||||||
border: none;
|
|
||||||
border-radius: 9px;
|
|
||||||
padding: 11px 18px;
|
|
||||||
color: white;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
QPushButton:hover {
|
|
||||||
background: #3475ef;
|
|
||||||
}
|
|
||||||
|
|
||||||
QPushButton:pressed {
|
|
||||||
background: #1f55ba;
|
|
||||||
}
|
|
||||||
|
|
||||||
QPushButton#secondary {
|
|
||||||
background: #242933;
|
|
||||||
color: #cbd1db;
|
|
||||||
}
|
|
||||||
|
|
||||||
QPushButton#secondary:hover {
|
|
||||||
background: #2d333f;
|
|
||||||
}
|
|
||||||
|
|
||||||
QListWidget {
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
|
||||||
outline: none;
|
|
||||||
padding: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
QListWidget::item {
|
|
||||||
padding: 8px 7px;
|
|
||||||
border-radius: 7px;
|
|
||||||
}
|
|
||||||
|
|
||||||
QListWidget::item:hover {
|
|
||||||
background: #20242d;
|
|
||||||
}
|
|
||||||
|
|
||||||
QListWidget::item:selected {
|
|
||||||
background: #202a3d;
|
|
||||||
color: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
QLabel#result {
|
|
||||||
color: #66d9a8;
|
|
||||||
font-size: 28px;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
QLabel#status {
|
|
||||||
color: #7e8795;
|
|
||||||
}
|
|
||||||
|
|
||||||
QSplitter::handle {
|
|
||||||
background: #111318;
|
|
||||||
width: 8px;
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class AbacusWindow(QMainWindow):
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__()
|
|
||||||
|
|
||||||
self.abacus = AbacusCore()
|
|
||||||
self.step = 0
|
|
||||||
|
|
||||||
self.setWindowTitle("Abacus")
|
|
||||||
self.resize(1100, 720)
|
|
||||||
self.setMinimumSize(850, 560)
|
|
||||||
|
|
||||||
self._build_ui()
|
|
||||||
self._update_lists()
|
|
||||||
|
|
||||||
def _build_ui(self):
|
|
||||||
root = QWidget()
|
|
||||||
root_layout = QVBoxLayout(root)
|
|
||||||
root_layout.setContentsMargins(18, 14, 18, 18)
|
|
||||||
root_layout.setSpacing(14)
|
|
||||||
|
|
||||||
# Header
|
|
||||||
topbar = QFrame()
|
|
||||||
topbar.setObjectName("topbar")
|
|
||||||
top_layout = QHBoxLayout(topbar)
|
|
||||||
top_layout.setContentsMargins(4, 4, 4, 12)
|
|
||||||
|
|
||||||
title_box = QVBoxLayout()
|
|
||||||
title_box.setSpacing(1)
|
|
||||||
|
|
||||||
title = QLabel("Abacus")
|
|
||||||
title.setObjectName("title")
|
|
||||||
|
|
||||||
subtitle = QLabel("Expression calculator")
|
|
||||||
subtitle.setObjectName("subtitle")
|
|
||||||
|
|
||||||
title_box.addWidget(title)
|
|
||||||
title_box.addWidget(subtitle)
|
|
||||||
top_layout.addLayout(title_box)
|
|
||||||
top_layout.addStretch()
|
|
||||||
|
|
||||||
self.status = QLabel("Ready")
|
|
||||||
self.status.setObjectName("status")
|
|
||||||
top_layout.addWidget(self.status)
|
|
||||||
|
|
||||||
root_layout.addWidget(topbar)
|
|
||||||
|
|
||||||
splitter = QSplitter(Qt.Horizontal)
|
|
||||||
splitter.setChildrenCollapsible(False)
|
|
||||||
|
|
||||||
# Left: history
|
|
||||||
history_panel = self._make_panel("History")
|
|
||||||
history_layout = history_panel.layout()
|
|
||||||
|
|
||||||
self.history = QListWidget()
|
|
||||||
self.history.setAlternatingRowColors(False)
|
|
||||||
history_layout.addWidget(self.history)
|
|
||||||
|
|
||||||
clear_btn = QPushButton("Clear history")
|
|
||||||
clear_btn.setObjectName("secondary")
|
|
||||||
clear_btn.clicked.connect(self.history.clear)
|
|
||||||
history_layout.addWidget(clear_btn)
|
|
||||||
|
|
||||||
splitter.addWidget(history_panel)
|
|
||||||
|
|
||||||
# Center
|
|
||||||
center = QWidget()
|
|
||||||
center_layout = QVBoxLayout(center)
|
|
||||||
center_layout.setContentsMargins(0, 0, 0, 0)
|
|
||||||
center_layout.setSpacing(14)
|
|
||||||
|
|
||||||
calc_panel = self._make_panel("Calculator")
|
|
||||||
calc_layout = calc_panel.layout()
|
|
||||||
|
|
||||||
self.result = QLabel("0")
|
|
||||||
self.result.setObjectName("result")
|
|
||||||
self.result.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
|
|
||||||
calc_layout.addWidget(self.result)
|
|
||||||
|
|
||||||
self.input = QLineEdit()
|
|
||||||
self.input.setObjectName("input")
|
|
||||||
self.input.setPlaceholderText("Enter expression, e.g. 2 + 3 * alpha")
|
|
||||||
self.input.returnPressed.connect(self.calculate)
|
|
||||||
calc_layout.addWidget(self.input)
|
|
||||||
|
|
||||||
buttons = QHBoxLayout()
|
|
||||||
buttons.setSpacing(8)
|
|
||||||
|
|
||||||
enter = QPushButton("Calculate")
|
|
||||||
enter.clicked.connect(self.calculate)
|
|
||||||
|
|
||||||
clear = QPushButton("Clear")
|
|
||||||
clear.setObjectName("secondary")
|
|
||||||
clear.clicked.connect(self._clear_input)
|
|
||||||
|
|
||||||
buttons.addWidget(enter)
|
|
||||||
buttons.addWidget(clear)
|
|
||||||
calc_layout.addLayout(buttons)
|
|
||||||
|
|
||||||
center_layout.addWidget(calc_panel)
|
|
||||||
|
|
||||||
help_panel = self._make_panel("Quick reference")
|
|
||||||
help_layout = help_panel.layout()
|
|
||||||
|
|
||||||
help_text = QLabel(
|
|
||||||
"Variable: alpha = 10\n"
|
|
||||||
"Function: double: alpha * 2\n"
|
|
||||||
"Comment: 2 + 3 # test\n"
|
|
||||||
"Commands from the original console are available through the UI."
|
|
||||||
)
|
|
||||||
help_text.setStyleSheet("color: #9da5b3; line-height: 1.5;")
|
|
||||||
help_text.setWordWrap(True)
|
|
||||||
help_layout.addWidget(help_text)
|
|
||||||
center_layout.addWidget(help_panel)
|
|
||||||
center_layout.addStretch()
|
|
||||||
|
|
||||||
splitter.addWidget(center)
|
|
||||||
|
|
||||||
# Right: variables/functions
|
|
||||||
right = QWidget()
|
|
||||||
right_layout = QVBoxLayout(right)
|
|
||||||
right_layout.setContentsMargins(0, 0, 0, 0)
|
|
||||||
right_layout.setSpacing(14)
|
|
||||||
|
|
||||||
var_panel = self._make_panel("Variables")
|
|
||||||
var_panel.layout().addWidget(QLabel("Stored values"))
|
|
||||||
self.variables = QListWidget()
|
|
||||||
var_panel.layout().addWidget(self.variables)
|
|
||||||
right_layout.addWidget(var_panel, 1)
|
|
||||||
|
|
||||||
func_panel = self._make_panel("Functions")
|
|
||||||
func_panel.layout().addWidget(QLabel("Definitions"))
|
|
||||||
self.functions = QListWidget()
|
|
||||||
func_panel.layout().addWidget(self.functions)
|
|
||||||
right_layout.addWidget(func_panel, 1)
|
|
||||||
|
|
||||||
splitter.addWidget(right)
|
|
||||||
|
|
||||||
splitter.setSizes([260, 500, 280])
|
|
||||||
root_layout.addWidget(splitter, 1)
|
|
||||||
|
|
||||||
self.setCentralWidget(root)
|
|
||||||
|
|
||||||
def _make_panel(self, title):
|
|
||||||
panel = QFrame()
|
|
||||||
panel.setObjectName("panel")
|
|
||||||
|
|
||||||
layout = QVBoxLayout(panel)
|
|
||||||
layout.setContentsMargins(14, 13, 14, 14)
|
|
||||||
layout.setSpacing(9)
|
|
||||||
|
|
||||||
label = QLabel(title)
|
|
||||||
label.setObjectName("panelTitle")
|
|
||||||
layout.addWidget(label)
|
|
||||||
|
|
||||||
return panel
|
|
||||||
|
|
||||||
def calculate(self):
|
|
||||||
text = self.input.text().strip()
|
|
||||||
if not text:
|
|
||||||
return
|
|
||||||
|
|
||||||
self.step += 1
|
|
||||||
|
|
||||||
try:
|
|
||||||
result = self.abacus.parse_input(text)
|
|
||||||
except Exception as exc:
|
|
||||||
self.status.setText(f"Error: {exc}")
|
|
||||||
self.result.setText("Error")
|
|
||||||
return
|
|
||||||
|
|
||||||
if not result:
|
|
||||||
self.input.clear()
|
|
||||||
self.status.setText("Comment / empty input")
|
|
||||||
return
|
|
||||||
|
|
||||||
original, _, value, comment = result
|
|
||||||
value_text = str(value)
|
|
||||||
|
|
||||||
line = f"{self.step}. {original} = {value_text}"
|
|
||||||
if comment:
|
|
||||||
line += f" #{comment}"
|
|
||||||
|
|
||||||
self.history.addItem(line)
|
|
||||||
self.history.scrollToBottom()
|
|
||||||
|
|
||||||
self.result.setText(value_text)
|
|
||||||
self.input.setText(value_text)
|
|
||||||
self.status.setText("Calculated")
|
|
||||||
self._update_lists()
|
|
||||||
|
|
||||||
def _clear_input(self):
|
|
||||||
self.input.clear()
|
|
||||||
self.result.setText("0")
|
|
||||||
self.status.setText("Ready")
|
|
||||||
self.input.setFocus()
|
|
||||||
|
|
||||||
def _update_lists(self):
|
|
||||||
self.variables.clear()
|
|
||||||
for key, value in self.abacus.get_vars().items():
|
|
||||||
self.variables.addItem(f"{key} = {value}")
|
|
||||||
|
|
||||||
self.functions.clear()
|
|
||||||
for key, value in self.abacus.get_funcs().items():
|
|
||||||
self.functions.addItem(f"{key} = {value}")
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
app = QApplication(sys.argv)
|
|
||||||
app.setStyle("Fusion")
|
|
||||||
app.setFont(QFont("Segoe UI", 10))
|
|
||||||
app.setStyleSheet(STYLE)
|
|
||||||
|
|
||||||
window = AbacusWindow()
|
|
||||||
window.show()
|
|
||||||
|
|
||||||
sys.exit(app.exec())
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
|
|
@ -1,360 +0,0 @@
|
||||||
import sys
|
|
||||||
|
|
||||||
from PySide6.QtCore import Qt
|
|
||||||
from PySide6.QtGui import QFont
|
|
||||||
from PySide6.QtWidgets import (
|
|
||||||
QApplication,
|
|
||||||
QFrame,
|
|
||||||
QHBoxLayout,
|
|
||||||
QLabel,
|
|
||||||
QLineEdit,
|
|
||||||
QListWidget,
|
|
||||||
QMainWindow,
|
|
||||||
QPushButton,
|
|
||||||
QSplitter,
|
|
||||||
QVBoxLayout,
|
|
||||||
QWidget,
|
|
||||||
)
|
|
||||||
|
|
||||||
from abacus_core import AbacusCore
|
|
||||||
|
|
||||||
|
|
||||||
STYLE = """
|
|
||||||
QMainWindow, QWidget {
|
|
||||||
background: #111318;
|
|
||||||
color: #e7e9ee;
|
|
||||||
font-family: "Segoe UI";
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
QFrame#topbar {
|
|
||||||
background: #111820;
|
|
||||||
border-bottom: 1px solid #292e38;
|
|
||||||
}
|
|
||||||
|
|
||||||
QLabel#title {
|
|
||||||
font-size: 22px;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #f5f7fb;
|
|
||||||
}
|
|
||||||
|
|
||||||
QLabel#subtitle {
|
|
||||||
color: #858c99;
|
|
||||||
}
|
|
||||||
|
|
||||||
QFrame#panel {
|
|
||||||
background: #111820;
|
|
||||||
border: 1px solid #202a36;
|
|
||||||
border-radius: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
QLabel#panelTitle {
|
|
||||||
color: #9da5b3;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
QLineEdit#input {
|
|
||||||
background: #0d0f13;
|
|
||||||
border: 1px solid #343a46;
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 12px 14px;
|
|
||||||
color: #ffffff;
|
|
||||||
font-size: 16px;
|
|
||||||
selection-background-color: #3b82f6;
|
|
||||||
}
|
|
||||||
|
|
||||||
QLineEdit#input:focus {
|
|
||||||
border: 1px solid #4b8cff;
|
|
||||||
}
|
|
||||||
|
|
||||||
QPushButton {
|
|
||||||
background: #2864d7;
|
|
||||||
border: none;
|
|
||||||
border-radius: 9px;
|
|
||||||
padding: 11px 18px;
|
|
||||||
color: white;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
QPushButton:hover {
|
|
||||||
background: #3475ef;
|
|
||||||
}
|
|
||||||
|
|
||||||
QPushButton:pressed {
|
|
||||||
background: #1f55ba;
|
|
||||||
}
|
|
||||||
|
|
||||||
QPushButton#secondary {
|
|
||||||
background: #242933;
|
|
||||||
color: #cbd1db;
|
|
||||||
}
|
|
||||||
|
|
||||||
QPushButton#secondary:hover {
|
|
||||||
background: #2d333f;
|
|
||||||
}
|
|
||||||
|
|
||||||
QListWidget {
|
|
||||||
background: #0d131a;
|
|
||||||
border: 1px solid #202a36;
|
|
||||||
border-radius: 10px;
|
|
||||||
outline: none;
|
|
||||||
padding: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
QListWidget::item {
|
|
||||||
padding: 8px 7px;
|
|
||||||
border-radius: 7px;
|
|
||||||
}
|
|
||||||
|
|
||||||
QListWidget::item:hover {
|
|
||||||
background: #20242d;
|
|
||||||
}
|
|
||||||
|
|
||||||
QListWidget::item:selected {
|
|
||||||
background: #202a3d;
|
|
||||||
color: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
QLabel#result {
|
|
||||||
color: #66d9a8;
|
|
||||||
font-size: 28px;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
QLabel#status {
|
|
||||||
color: #7e8795;
|
|
||||||
}
|
|
||||||
|
|
||||||
QSplitter::handle {
|
|
||||||
background: #111318;
|
|
||||||
width: 8px;
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class AbacusWindow(QMainWindow):
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__()
|
|
||||||
|
|
||||||
self.abacus = AbacusCore()
|
|
||||||
self.step = 0
|
|
||||||
|
|
||||||
self.setWindowTitle("Abacus")
|
|
||||||
self.resize(1180, 760)
|
|
||||||
self.setMinimumSize(850, 560)
|
|
||||||
|
|
||||||
self._build_ui()
|
|
||||||
self._update_lists()
|
|
||||||
|
|
||||||
def _build_ui(self):
|
|
||||||
root = QWidget()
|
|
||||||
root_layout = QVBoxLayout(root)
|
|
||||||
root_layout.setContentsMargins(18, 14, 18, 18)
|
|
||||||
root_layout.setSpacing(14)
|
|
||||||
|
|
||||||
# Header
|
|
||||||
topbar = QFrame()
|
|
||||||
topbar.setObjectName("topbar")
|
|
||||||
top_layout = QHBoxLayout(topbar)
|
|
||||||
top_layout.setContentsMargins(4, 4, 4, 12)
|
|
||||||
|
|
||||||
title_box = QVBoxLayout()
|
|
||||||
title_box.setSpacing(1)
|
|
||||||
|
|
||||||
title = QLabel("Abacus")
|
|
||||||
title.setObjectName("title")
|
|
||||||
|
|
||||||
subtitle = QLabel("Expression calculator")
|
|
||||||
subtitle.setObjectName("subtitle")
|
|
||||||
|
|
||||||
title_box.addWidget(title)
|
|
||||||
title_box.addWidget(subtitle)
|
|
||||||
top_layout.addLayout(title_box)
|
|
||||||
top_layout.addStretch()
|
|
||||||
|
|
||||||
self.status = QLabel("Ready")
|
|
||||||
self.status.setObjectName("status")
|
|
||||||
top_layout.addWidget(self.status)
|
|
||||||
|
|
||||||
root_layout.addWidget(topbar)
|
|
||||||
|
|
||||||
splitter = QSplitter(Qt.Horizontal)
|
|
||||||
splitter.setChildrenCollapsible(False)
|
|
||||||
|
|
||||||
# Left: history
|
|
||||||
history_panel = self._make_panel("History")
|
|
||||||
history_layout = history_panel.layout()
|
|
||||||
|
|
||||||
self.history = QListWidget()
|
|
||||||
self.history.setAlternatingRowColors(False)
|
|
||||||
history_layout.addWidget(self.history)
|
|
||||||
|
|
||||||
clear_btn = QPushButton("Clear history")
|
|
||||||
clear_btn.setObjectName("secondary")
|
|
||||||
clear_btn.clicked.connect(self.history.clear)
|
|
||||||
history_layout.addWidget(clear_btn)
|
|
||||||
|
|
||||||
splitter.addWidget(history_panel)
|
|
||||||
|
|
||||||
# Center
|
|
||||||
center = QWidget()
|
|
||||||
center_layout = QVBoxLayout(center)
|
|
||||||
center_layout.setContentsMargins(0, 0, 0, 0)
|
|
||||||
center_layout.setSpacing(14)
|
|
||||||
|
|
||||||
calc_panel = self._make_panel("Calculator")
|
|
||||||
calc_layout = calc_panel.layout()
|
|
||||||
|
|
||||||
self.result = QLabel("0")
|
|
||||||
self.result.setObjectName("result")
|
|
||||||
self.result.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
|
|
||||||
calc_layout.addWidget(self.result)
|
|
||||||
|
|
||||||
self.input = QLineEdit()
|
|
||||||
self.input.setObjectName("input")
|
|
||||||
self.input.setPlaceholderText("Enter expression, e.g. 2 + 3 * alpha")
|
|
||||||
self.input.returnPressed.connect(self.calculate)
|
|
||||||
calc_layout.addWidget(self.input)
|
|
||||||
|
|
||||||
buttons = QHBoxLayout()
|
|
||||||
buttons.setSpacing(8)
|
|
||||||
|
|
||||||
enter = QPushButton("Calculate")
|
|
||||||
enter.clicked.connect(self.calculate)
|
|
||||||
|
|
||||||
clear = QPushButton("Clear")
|
|
||||||
clear.setObjectName("secondary")
|
|
||||||
clear.clicked.connect(self._clear_input)
|
|
||||||
|
|
||||||
buttons.addWidget(enter)
|
|
||||||
buttons.addWidget(clear)
|
|
||||||
calc_layout.addLayout(buttons)
|
|
||||||
|
|
||||||
center_layout.addWidget(calc_panel)
|
|
||||||
|
|
||||||
help_panel = self._make_panel("Quick reference")
|
|
||||||
help_layout = help_panel.layout()
|
|
||||||
|
|
||||||
help_text = QLabel(
|
|
||||||
"Variable: alpha = 10\n"
|
|
||||||
"Function: double: alpha * 2\n"
|
|
||||||
"Comment: 2 + 3 # test\n"
|
|
||||||
"Commands from the original console are available through the UI."
|
|
||||||
)
|
|
||||||
help_text.setStyleSheet("color: #9da5b3; line-height: 1.5;")
|
|
||||||
help_text.setWordWrap(True)
|
|
||||||
help_layout.addWidget(help_text)
|
|
||||||
center_layout.addWidget(help_panel)
|
|
||||||
center_layout.addStretch()
|
|
||||||
|
|
||||||
splitter.addWidget(center)
|
|
||||||
|
|
||||||
# Right: variables/functions
|
|
||||||
right = QWidget()
|
|
||||||
right_layout = QVBoxLayout(right)
|
|
||||||
right_layout.setContentsMargins(0, 0, 0, 0)
|
|
||||||
right_layout.setSpacing(14)
|
|
||||||
|
|
||||||
var_panel = self._make_panel("Variables")
|
|
||||||
var_panel.layout().addWidget(QLabel("Stored values"))
|
|
||||||
self.variables = QListWidget()
|
|
||||||
var_panel.layout().addWidget(self.variables)
|
|
||||||
right_layout.addWidget(var_panel, 1)
|
|
||||||
|
|
||||||
func_panel = self._make_panel("Functions")
|
|
||||||
func_panel.layout().addWidget(QLabel("Definitions"))
|
|
||||||
self.functions = QListWidget()
|
|
||||||
func_panel.layout().addWidget(self.functions)
|
|
||||||
right_layout.addWidget(func_panel, 1)
|
|
||||||
|
|
||||||
splitter.addWidget(right)
|
|
||||||
|
|
||||||
splitter.setSizes([260, 500, 280])
|
|
||||||
root_layout.addWidget(splitter, 1)
|
|
||||||
|
|
||||||
self.setCentralWidget(root)
|
|
||||||
|
|
||||||
def _make_panel(self, title):
|
|
||||||
panel = QFrame()
|
|
||||||
panel.setObjectName("panel")
|
|
||||||
|
|
||||||
layout = QVBoxLayout(panel)
|
|
||||||
layout.setContentsMargins(14, 13, 14, 14)
|
|
||||||
layout.setSpacing(9)
|
|
||||||
|
|
||||||
label = QLabel(title)
|
|
||||||
label.setObjectName("panelTitle")
|
|
||||||
layout.addWidget(label)
|
|
||||||
|
|
||||||
return panel
|
|
||||||
|
|
||||||
def calculate(self):
|
|
||||||
text = self.input.text().strip()
|
|
||||||
if not text:
|
|
||||||
return
|
|
||||||
|
|
||||||
self.step += 1
|
|
||||||
|
|
||||||
try:
|
|
||||||
result = self.abacus.parse_input(text)
|
|
||||||
except Exception as exc:
|
|
||||||
self.step -= 1
|
|
||||||
self.status.setText(f"Error: {exc}")
|
|
||||||
self.result.setText("Error")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Assignments return None from AbacusCore.parse_input().
|
|
||||||
# Refresh the side panels BEFORE handling the result.
|
|
||||||
self._update_lists()
|
|
||||||
|
|
||||||
if not result:
|
|
||||||
self.input.clear()
|
|
||||||
self.status.setText("Updated")
|
|
||||||
return
|
|
||||||
|
|
||||||
original, _, value, comment = result
|
|
||||||
value_text = str(value)
|
|
||||||
|
|
||||||
line = f"{self.step}. {original} = {value_text}"
|
|
||||||
if comment:
|
|
||||||
line += f" #{comment}"
|
|
||||||
|
|
||||||
self.history.addItem(line)
|
|
||||||
self.history.scrollToBottom()
|
|
||||||
|
|
||||||
self.result.setText(value_text)
|
|
||||||
self.input.setText(value_text)
|
|
||||||
self.status.setText("Calculated")
|
|
||||||
self._update_lists()
|
|
||||||
|
|
||||||
def _clear_input(self):
|
|
||||||
self.input.clear()
|
|
||||||
self.result.setText("0")
|
|
||||||
self.status.setText("Ready")
|
|
||||||
self.input.setFocus()
|
|
||||||
|
|
||||||
def _update_lists(self):
|
|
||||||
self.variables.clear()
|
|
||||||
for key, value in self.abacus.get_vars().items():
|
|
||||||
self.variables.addItem(f"{key} = {value}")
|
|
||||||
|
|
||||||
self.functions.clear()
|
|
||||||
for key, value in self.abacus.get_funcs().items():
|
|
||||||
self.functions.addItem(f"{key} = {value}")
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
app = QApplication(sys.argv)
|
|
||||||
app.setStyle("Fusion")
|
|
||||||
app.setFont(QFont("Segoe UI", 10))
|
|
||||||
app.setStyleSheet(STYLE)
|
|
||||||
|
|
||||||
window = AbacusWindow()
|
|
||||||
window.show()
|
|
||||||
|
|
||||||
sys.exit(app.exec())
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
20
console.py
20
console.py
|
|
@ -1,20 +0,0 @@
|
||||||
from abacus_core import AbacusCore
|
|
||||||
|
|
||||||
ab = AbacusCore()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
step = 0
|
|
||||||
res = 0
|
|
||||||
while True:
|
|
||||||
var = input('gib was ein:')
|
|
||||||
if var == 'vars':
|
|
||||||
print(ab.get_vars())
|
|
||||||
elif var == 'func':
|
|
||||||
print(ab.get_funcs())
|
|
||||||
elif var == 'res':
|
|
||||||
print(res)
|
|
||||||
else:
|
|
||||||
step += 1
|
|
||||||
res = ab.parse_input(str(var))
|
|
||||||
print('step', step, 'result', res)
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
{
|
|
||||||
"format": "abacus-history",
|
|
||||||
"version": 1,
|
|
||||||
"step": 4,
|
|
||||||
"document": "dieselpreis=1,99\nbasisfracht=200\npercent=20\nbasisfracht*(percent/100)*dieselpreis",
|
|
||||||
"variables": {
|
|
||||||
"result": 79.6,
|
|
||||||
"alphabet": 28,
|
|
||||||
"alpha": 1,
|
|
||||||
"beta": 2,
|
|
||||||
"dieselpreis": 1.99,
|
|
||||||
"basisfracht": 200,
|
|
||||||
"percent": 20
|
|
||||||
},
|
|
||||||
"functions": {},
|
|
||||||
"history": [
|
|
||||||
"4. basisfracht*(percent/100)*dieselpreis = 79.6"
|
|
||||||
],
|
|
||||||
"results": [
|
|
||||||
{
|
|
||||||
"step": 4,
|
|
||||||
"expression": "basisfracht*(percent/100)*dieselpreis",
|
|
||||||
"result": "79.6",
|
|
||||||
"comment": ""
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"graph_values": [
|
|
||||||
79.6
|
|
||||||
]
|
|
||||||
}
|
|
||||||
194
gui.ui
194
gui.ui
|
|
@ -1,194 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<ui version="4.0">
|
|
||||||
<class>MainWindow</class>
|
|
||||||
<widget class="QMainWindow" name="MainWindow">
|
|
||||||
<property name="geometry">
|
|
||||||
<rect>
|
|
||||||
<x>0</x>
|
|
||||||
<y>0</y>
|
|
||||||
<width>735</width>
|
|
||||||
<height>770</height>
|
|
||||||
</rect>
|
|
||||||
</property>
|
|
||||||
<property name="windowTitle">
|
|
||||||
<string>AbacusNG</string>
|
|
||||||
</property>
|
|
||||||
<widget class="QWidget" name="centralwidget">
|
|
||||||
<layout class="QVBoxLayout" name="verticalLayout_5">
|
|
||||||
<item>
|
|
||||||
<layout class="QHBoxLayout" name="horizontalLayout_4">
|
|
||||||
<item>
|
|
||||||
<layout class="QVBoxLayout" name="verticalLayout_4">
|
|
||||||
<item>
|
|
||||||
<widget class="QLabel" name="label_3">
|
|
||||||
<property name="text">
|
|
||||||
<string>Verlauf</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<widget class="QListView" name="lv_history"/>
|
|
||||||
</item>
|
|
||||||
</layout>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<layout class="QVBoxLayout" name="verticalLayout_3">
|
|
||||||
<item>
|
|
||||||
<widget class="QLabel" name="label">
|
|
||||||
<property name="text">
|
|
||||||
<string>Variablen</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
|
||||||
<item>
|
|
||||||
<widget class="QListView" name="lv_variable">
|
|
||||||
<property name="selectionMode">
|
|
||||||
<enum>QAbstractItemView::SelectionMode::SingleSelection</enum>
|
|
||||||
</property>
|
|
||||||
<property name="resizeMode">
|
|
||||||
<enum>QListView::ResizeMode::Adjust</enum>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<layout class="QHBoxLayout" name="horizontalLayout_3">
|
|
||||||
<item>
|
|
||||||
<widget class="QPushButton" name="but_var_new">
|
|
||||||
<property name="text">
|
|
||||||
<string>Neu</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<widget class="QPushButton" name="but_var_edit">
|
|
||||||
<property name="text">
|
|
||||||
<string>Bearbeiten</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<widget class="QPushButton" name="but_var_remove">
|
|
||||||
<property name="text">
|
|
||||||
<string>Löschen</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
</layout>
|
|
||||||
</item>
|
|
||||||
</layout>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<widget class="QLabel" name="label_2">
|
|
||||||
<property name="text">
|
|
||||||
<string>Funktionen</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<layout class="QVBoxLayout" name="verticalLayout">
|
|
||||||
<item>
|
|
||||||
<widget class="QListView" name="lv_func"/>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
|
||||||
<item>
|
|
||||||
<widget class="QPushButton" name="but_func_new">
|
|
||||||
<property name="text">
|
|
||||||
<string>Neu</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<widget class="QPushButton" name="but_func_edit">
|
|
||||||
<property name="text">
|
|
||||||
<string>Bearbeiten</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<widget class="QPushButton" name="but_func_remove">
|
|
||||||
<property name="text">
|
|
||||||
<string>Löschnen</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
</layout>
|
|
||||||
</item>
|
|
||||||
</layout>
|
|
||||||
</item>
|
|
||||||
</layout>
|
|
||||||
</item>
|
|
||||||
</layout>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
|
||||||
<item>
|
|
||||||
<widget class="QLineEdit" name="le_input"/>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<widget class="QPushButton" name="but_enter">
|
|
||||||
<property name="text">
|
|
||||||
<string>Rechne!</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
</layout>
|
|
||||||
</item>
|
|
||||||
</layout>
|
|
||||||
</widget>
|
|
||||||
<widget class="QMenuBar" name="menubar">
|
|
||||||
<property name="geometry">
|
|
||||||
<rect>
|
|
||||||
<x>0</x>
|
|
||||||
<y>0</y>
|
|
||||||
<width>735</width>
|
|
||||||
<height>24</height>
|
|
||||||
</rect>
|
|
||||||
</property>
|
|
||||||
<widget class="QMenu" name="menuDatei">
|
|
||||||
<property name="title">
|
|
||||||
<string>Datei</string>
|
|
||||||
</property>
|
|
||||||
<addaction name="actionNeu"/>
|
|
||||||
<addaction name="separator"/>
|
|
||||||
<addaction name="actionSpeichern"/>
|
|
||||||
<addaction name="actionSpeichern_unter"/>
|
|
||||||
<addaction name="separator"/>
|
|
||||||
<addaction name="actionDrucken"/>
|
|
||||||
<addaction name="separator"/>
|
|
||||||
<addaction name="actionExit"/>
|
|
||||||
</widget>
|
|
||||||
<addaction name="menuDatei"/>
|
|
||||||
</widget>
|
|
||||||
<widget class="QStatusBar" name="statusbar"/>
|
|
||||||
<action name="actionNeu">
|
|
||||||
<property name="text">
|
|
||||||
<string>Neu</string>
|
|
||||||
</property>
|
|
||||||
</action>
|
|
||||||
<action name="actionSpeichern">
|
|
||||||
<property name="text">
|
|
||||||
<string>Speichern</string>
|
|
||||||
</property>
|
|
||||||
</action>
|
|
||||||
<action name="actionSpeichern_unter">
|
|
||||||
<property name="text">
|
|
||||||
<string>Speichern unter</string>
|
|
||||||
</property>
|
|
||||||
</action>
|
|
||||||
<action name="actionDrucken">
|
|
||||||
<property name="text">
|
|
||||||
<string>Drucken</string>
|
|
||||||
</property>
|
|
||||||
</action>
|
|
||||||
<action name="actionExit">
|
|
||||||
<property name="text">
|
|
||||||
<string>Beenden</string>
|
|
||||||
</property>
|
|
||||||
</action>
|
|
||||||
</widget>
|
|
||||||
<resources/>
|
|
||||||
<connections/>
|
|
||||||
</ui>
|
|
||||||
75
main.py
75
main.py
|
|
@ -1,76 +1,7 @@
|
||||||
import sys
|
|
||||||
|
|
||||||
from PySide6.QtGui import QStandardItemModel, QStandardItem
|
|
||||||
from PySide6.QtUiTools import QUiLoader
|
|
||||||
from PySide6.QtWidgets import QApplication, QFileDialog
|
|
||||||
from PySide6.QtCore import QFile, QIODevice, QModelIndex, QAbstractTableModel
|
|
||||||
from abacus_core import AbacusCore
|
from abacus_core import AbacusCore
|
||||||
|
|
||||||
|
ab = AbacusCore()
|
||||||
class AbacusGUI:
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self._abacus_core = AbacusCore()
|
|
||||||
self._step = 0
|
|
||||||
self._history = []
|
|
||||||
self._app = QApplication(sys.argv)
|
|
||||||
ui_file_name = "gui.ui"
|
|
||||||
ui_file = QFile(ui_file_name)
|
|
||||||
if not ui_file.open(QIODevice.ReadOnly):
|
|
||||||
print(f"Cannot open {ui_file_name}: {ui_file.errorString()}")
|
|
||||||
sys.exit(-1)
|
|
||||||
loader = QUiLoader()
|
|
||||||
self._window = loader.load(ui_file)
|
|
||||||
ui_file.close()
|
|
||||||
if not self._window:
|
|
||||||
print(loader.errorString())
|
|
||||||
sys.exit(-1)
|
|
||||||
self._window.show()
|
|
||||||
self._window.but_enter.clicked.connect(self._berechne)
|
|
||||||
self._window.le_input.returnPressed.connect(self._berechne)
|
|
||||||
self._history_model = QStandardItemModel()
|
|
||||||
self._window.lv_history.setModel(self._history_model)
|
|
||||||
self._var_model = QStandardItemModel()
|
|
||||||
self._func_model = QStandardItemModel()
|
|
||||||
self._window.lv_variable.setModel(self._var_model)
|
|
||||||
self._window.lv_func.setModel(self._func_model)
|
|
||||||
# self._window.actionExit.clicked.connect(self._berechne)
|
|
||||||
self._check_models()
|
|
||||||
sys.exit(self._app.exec())
|
|
||||||
|
|
||||||
def _berechne(self):
|
|
||||||
self._step += 1
|
|
||||||
input_str = self._window.le_input.text()
|
|
||||||
result = self._abacus_core.parse_input(input_str)
|
|
||||||
if result:
|
|
||||||
self._history.append(result)
|
|
||||||
input_str = result[0]
|
|
||||||
input_wo_var = str(result[1])
|
|
||||||
ergebnis = str(result[2])
|
|
||||||
comment = result[3]
|
|
||||||
res_str = str(self._step) + '. ' + input_str + ' = ' + ergebnis
|
|
||||||
# res_str = str(self._step) + '. ' + input_str + ' => ' + input_wo_var + ' = ' + ergebnis
|
|
||||||
if comment:
|
|
||||||
res_str += ' ' + comment
|
|
||||||
item = QStandardItem(res_str)
|
|
||||||
self._history_model.appendRow(item)
|
|
||||||
res = result[2]
|
|
||||||
self._window.le_input.setText(str(res))
|
|
||||||
else:
|
|
||||||
self._window.le_input.setText('')
|
|
||||||
self._check_models()
|
|
||||||
|
|
||||||
def _check_models(self):
|
|
||||||
self._var_model.clear()
|
|
||||||
self._func_model.clear()
|
|
||||||
for k, v in self._abacus_core.get_vars().items():
|
|
||||||
item = QStandardItem(k+' = '+str(v))
|
|
||||||
self._var_model.appendRow(item)
|
|
||||||
for k, v in self._abacus_core.get_funcs().items():
|
|
||||||
item = QStandardItem(k+' = '+str(v))
|
|
||||||
self._func_model.appendRow(item)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == '__main__':
|
||||||
ag = AbacusGUI()
|
ab.parse_input('12')
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
# AbacusNG
|
|
||||||
|
|
||||||
## Sinn und Zweck
|
|
||||||
|
|
||||||
## Known Bugs
|
|
||||||
- Eine ein-zeichen Variable so wie _ oder a funktioniert nicht
|
|
||||||
- rückgabewert einer funktion lässt sich nicht einer variablen zuweisen
|
|
||||||
46
test.json
46
test.json
|
|
@ -1,46 +0,0 @@
|
||||||
{
|
|
||||||
"format": "abacus-history",
|
|
||||||
"version": 1,
|
|
||||||
"step": 7,
|
|
||||||
"document": "alpha=35\nbeta=22\nalpha+beta\ndouble:result*2\ndouble\ntriple:result*3\ntriple+double+22",
|
|
||||||
"variables": {
|
|
||||||
"result": 592,
|
|
||||||
"alphabet": 28,
|
|
||||||
"alpha": 35,
|
|
||||||
"beta": 22
|
|
||||||
},
|
|
||||||
"functions": {
|
|
||||||
"double": "result*2",
|
|
||||||
"triple": "result*3"
|
|
||||||
},
|
|
||||||
"history": [
|
|
||||||
"3. alpha+beta = 57",
|
|
||||||
"5. double = 114",
|
|
||||||
"7. triple+double+22 = 592"
|
|
||||||
],
|
|
||||||
"results": [
|
|
||||||
{
|
|
||||||
"step": 3,
|
|
||||||
"expression": "alpha+beta",
|
|
||||||
"result": "57",
|
|
||||||
"comment": ""
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"step": 5,
|
|
||||||
"expression": "double",
|
|
||||||
"result": "114",
|
|
||||||
"comment": ""
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"step": 7,
|
|
||||||
"expression": "triple+double+22",
|
|
||||||
"result": "592",
|
|
||||||
"comment": ""
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"graph_values": [
|
|
||||||
57.0,
|
|
||||||
114.0,
|
|
||||||
592.0
|
|
||||||
]
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue