diff --git a/main.py b/main.py index 4c066dc2..6b11ac1d 100644 --- a/main.py +++ b/main.py @@ -13,55 +13,9 @@ sys.path.insert(0, os.path.join( os.path.dirname(__file__), "..", ".." )) -SOURCE_TEST = \ -'''def build_dataset(a, b): - """ Build you dataset. """ - return [0, a, 0, b, 1, a] -''' - -SOURCE_TEST_2 = \ -'''def mon_ia(a, b): - """ Compute the absolute value of inputs difference. """ - if a > b: - return a - b - else: - return b - a -''' - if __name__ == '__main__': app = QApplication(sys.argv) app.setStyle('Fusion') - wnd = OCBWindow() - # if hasattr(wnd, 'ocb_widget'): - # scene = wnd.ocb_widget.scene - - # test_block = OCBBlock(title="Other kind of block") - # scene.addItem(test_block) - # test_block.setPos(-250, 150) - - # test_block_2 = OCBCodeBlock(title="Dataset", source=SOURCE_TEST) - # for _ in range(2): - # test_block_2.add_socket(OCBSocket(test_block_2, socket_type='input')) - # for _ in range(1): - # test_block_2.add_socket(OCBSocket(test_block_2, socket_type='output')) - # test_block_2.setPos(-350, -100) - # scene.addItem(test_block_2) - - # test_block_3 = OCBCodeBlock(title="Mon IA (par blocks ?)", source=SOURCE_TEST_2) - # for _ in range(2): - # test_block_3.add_socket(OCBSocket(test_block_3, socket_type='input')) - # for _ in range(1): - # test_block_3.add_socket(OCBSocket(test_block_3, socket_type='output')) - # test_block_3.setPos(0, -100) - # scene.addItem(test_block_3) - - # # for i in range(3): - # # edge = OCBEdge( - # # source_socket=test_block_3.sockets_out[0], - # # destination_socket=test_block_2.sockets_in[i] - # # ) - # # scene.addItem(edge) - wnd.show() sys.exit(app.exec_()) diff --git a/opencodeblocks/graphics/pyeditor.py b/opencodeblocks/graphics/pyeditor.py index 88e6c1d3..f7127052 100644 --- a/opencodeblocks/graphics/pyeditor.py +++ b/opencodeblocks/graphics/pyeditor.py @@ -7,17 +7,19 @@ from PyQt5.QtCore import Qt from PyQt5.QtGui import QFocusEvent, QFont, QFontMetrics, QColor from PyQt5.Qsci import QsciScintilla, QsciLexerPython +from opencodeblocks.graphics.theme_manager import theme_manager from opencodeblocks.graphics.blocks.block import OCBBlock + if TYPE_CHECKING: from opencodeblocks.graphics.view import OCBView class PythonEditor(QsciScintilla): """ In-block python editor for OpenCodeBlocks. """ - - def __init__(self, block:OCBBlock): + + def __init__(self, block: OCBBlock): """ In-block python editor for OpenCodeBlocks. Args: @@ -28,50 +30,8 @@ def __init__(self, block:OCBBlock): self.block = block self.setText(self.block.source) - # Set the default font - font = QFont() - font.setFamily('Courier') - font.setFixedPitch(True) - font.setPointSize(1) - self.setFont(font) - - # Margin 0 is used for line numbers - fontmetrics = QFontMetrics(font) - foreground_color = QColor("#dddddd") - background_color = QColor("#212121") - self.setMarginsFont(font) - self.setMarginWidth(2, fontmetrics.width("00") + 6) - self.setMarginLineNumbers(2, True) - self.setMarginsForegroundColor(foreground_color) - self.setMarginsBackgroundColor(background_color) - - # Set Python lexer - lexer = QsciLexerPython() - lexer.setDefaultFont(font) - lexer.setDefaultPaper(QColor("#1E1E1E")) - lexer.setDefaultColor(QColor("#D4D4D4")) - - string_types = [ - QsciLexerPython.SingleQuotedString, - QsciLexerPython.DoubleQuotedString, - QsciLexerPython.UnclosedString, - QsciLexerPython.SingleQuotedFString, - QsciLexerPython.TripleSingleQuotedString, - QsciLexerPython.TripleDoubleQuotedString, - QsciLexerPython.TripleSingleQuotedFString, - QsciLexerPython.TripleDoubleQuotedFString, - ] - - for string_type in string_types: - lexer.setColor(QColor('#CE9178'), string_type) - - lexer.setColor(QColor('#DCDCAA'), QsciLexerPython.FunctionMethodName) - lexer.setColor(QColor('#569CD6'), QsciLexerPython.Keyword) - lexer.setColor(QColor('#4EC9B0'), QsciLexerPython.ClassName) - lexer.setColor(QColor('#7FB347'), QsciLexerPython.Number) - lexer.setColor(QColor('#D8D8D8'), QsciLexerPython.Operator) - - self.setLexer(lexer) + self.update_theme() + theme_manager().themeChanged.connect(self.update_theme) # Set caret self.setCaretForegroundColor(QColor("#D4D4D4")) @@ -97,6 +57,29 @@ def __init__(self, block:OCBBlock): self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground) self.setWindowFlags(Qt.WindowType.FramelessWindowHint) + def update_theme(self): + """ Change the font and colors of the editor to match the current theme """ + font = QFont() + font.setFamily(theme_manager().recommended_font_family) + font.setFixedPitch(True) + font.setPointSize(11) + self.setFont(font) + + # Margin 0 is used for line numbers + fontmetrics = QFontMetrics(font) + foreground_color = QColor("#dddddd") + background_color = QColor("#212121") + self.setMarginsFont(font) + self.setMarginWidth(2, fontmetrics.width("00") + 6) + self.setMarginLineNumbers(2, True) + self.setMarginsForegroundColor(foreground_color) + self.setMarginsBackgroundColor(background_color) + + lexer = QsciLexerPython() + theme_manager().current_theme().apply_to_lexer(lexer) + lexer.setFont(font) + self.setLexer(lexer) + def views(self) -> List['OCBView']: """ Get the views in which the python_editor is present. """ return self.graphicsProxyWidget().scene().views() diff --git a/opencodeblocks/graphics/qss/ocb_dark.qss b/opencodeblocks/graphics/qss/ocb_dark.qss index 2dc652c0..1934deed 100644 --- a/opencodeblocks/graphics/qss/ocb_dark.qss +++ b/opencodeblocks/graphics/qss/ocb_dark.qss @@ -1 +1,180 @@ -QFrame,QDialog,QMainWindow{background:#474747}QSplitter,QMainWindow::separator{background:#474747}QStatusBar{background:#474747;color:#ccc}QTabWidget{border:0}QTabBar{background:#474747;color:#ccc}QMdiArea QTabBar,QMdiArea QTabWidget,QMdiArea QTabWidget::pane,QMdiArea QTabWidget::tab-bar,QMdiArea QTabBar::tab{height:17px}QMdiArea QTabBar::tab:top:!selected,QMdiArea QTabBar::tab:top:selected,QMdiArea QTabBar::tab:top:!selected:hover{border-top-left-radius:4px;border-top-right-radius:4px;padding:2px 8px;padding-top:0;padding-bottom:3px;min-width:8ex;border:1px solid #333;border-bottom:0}QMdiArea QTabBar::tab:top:!selected,QMdiArea QTabBar::tab:top:!selected:hover{background:qlineargradient(x1 : 0,y1 : 0,x2 : 0,y2 : 1,stop : 0 #6d6d6d,stop : .1 #474747,stop : .89 #3f3f3f,stop : 1 #3f3f3f)}QMdiArea QTabBar::tab:top:selected{background:qlineargradient(x1 : 0,y1 : 0,x2 : 0,y2 : 1,stop : 0 #878787,stop : .1 #545454,stop : .89 #474747,stop : 1 #474747)}QMdiArea QTabBar::tab:top:!selected:hover{background:qlineargradient(x1 : 0,y1 : 0,x2 : 0,y2 : 1,stop : 0 #727272,stop : .1 #4c4c4c,stop : .89 #444,stop : 1 #444)}QMdiArea QTabBar QToolButton{background:qlineargradient(x1 : 0,y1 : 0,x2 : 0,y2 : 1,stop : 0 #878787,stop : .1 #616161,stop : .89 #4f4f4f,stop : 1 #4f4f4f);border:1px solid #333;border-radius:0}QMdiArea QTabBar QToolButton::left-arrow{image:url(":icons/small_arrow_left-light.png")}QMdiArea QTabBar QToolButton::right-arrow{image:url(":icons/small_arrow_right-light.png")}QMdiArea QTabBar::close-button:selected{image:url(":icons/tab_close_btn.png");subcontrol-origin:border;subcontrol-position:right bottom}QMdiArea QTabBar::close-button:!selected{image:url(":icons/tab_close_nonselected_btn.png")}QMdiSubWindow{border-style:solid;background:#616161}QTabBar::tab:selected,QTabBar::tab:hover{color:#eee}QDockWidget{color:#ddd;font-weight:bold;titlebar-close-icon:url(":icons/docktitle-close-btn-light.png");titlebar-normal-icon:url(":icons/docktitle-normal-btn-light.png")}QDockWidget::title{background:qlineargradient(x1 : 0,y1 : 0,x2 : 0,y2 : 1,stop : 0 #3b3b3b,stop : 1 #2e2e2e);padding-top:4px;padding-right:22px;font-weight:bold}QDockWidget::close-button,QDockWidget::float-button{subcontrol-position:top right;subcontrol-origin:margin;text-align:center;icon-size:16px;width:14px;position:absolute;top:0;bottom:0;left:0;right:4px}QDockWidget::close-button{right:4px}QDockWidget::float-button{right:18px}QMenuBar{background:#474747}QMenuBar::item{spacing:3px;padding:3px 5px;color:#eee;background:transparent}QMenuBar::item:selected,QMenuBar::item:pressed{background:#4f9eee}QMenu{background:#474747;border:1px solid #2e2e2e}QMenu::item{background:#474747;color:#eee}QMenu::item:selected{background:#616161}QMenu::active{background:#616161;color:#eee}QMenu::separator{height:1px;background:#2e2e2e}QMenu::disabled,QMenu::item:disabled{color:#6e6e6e}QListView{background-color:#555;alternate-background-color:#434343}QListView::item{height:22px;color:#e6e6e6}QListView::item:hover{background:#6e6e6e}QListView::item::active:hover{color:#fff}QListView::item:selected,QListView::item::active:selected{color:#fff;background:qlineargradient(x1 : 0,y1 : 0,x2 : 0,y2 : 1,stop : 0 #4f9eee,stop : 1 #2084ea);border:0}QPushButton{color:#e6e6e6;background:#555;border-color:#141414}QLabel{color:#e6e6e6}QLineEdit,QTextEdit{color:#e6e6e6;background:#5a5a5a}QLineEdit{border:1px solid #3a3a3a;border-radius:2px;padding:1px 2px}QDMNodeContentWidget{background:transparent;}QDMNodeContentWidget QFrame{background:transparent}QDMNodeContentWidget QTextEdit{background:#666}QDMNodeContentWidget QLabel{color:#e0e0e0}QGraphicsView{selection-background-color:#fff} \ No newline at end of file +QFrame,QDialog,QMainWindow{ + background:#474747 +} +QSplitter,QMainWindow::separator{ + background:#474747 +} +QStatusBar{ + background:#474747; + color:#ccc +} +QTabWidget{ + border:0 +} +QTabBar{ + background:#474747; + color:#ccc +} +QMdiArea QTabBar,QMdiArea QTabWidget,QMdiArea QTabWidget::pane,QMdiArea QTabWidget::tab-bar,QMdiArea QTabBar::tab{ + height:17px +} +QMdiArea QTabBar::tab:top:!selected,QMdiArea QTabBar::tab:top:selected,QMdiArea QTabBar::tab:top:!selected:hover{ + border-top-left-radius:4px; + border-top-right-radius:4px; + padding:2px 8px; + padding-top:0; + padding-bottom:3px; + min-width:8ex; + border:1px solid #333; + border-bottom:0 +} +QMdiArea QTabBar::tab:top:!selected,QMdiArea QTabBar::tab:top:!selected:hover{ + background:qlineargradient(x1 : 0,y1 : 0,x2 : 0,y2 : 1,stop : 0 #6d6d6d,stop : .1 #474747,stop : .89 #3f3f3f,stop : 1 #3f3f3f) +} +QMdiArea QTabBar::tab:top:selected{ + background:qlineargradient(x1 : 0,y1 : 0,x2 : 0,y2 : 1,stop : 0 #878787,stop : .1 #545454,stop : .89 #474747,stop : 1 #474747) +} +QMdiArea QTabBar::tab:top:!selected:hover{ + background:qlineargradient(x1 : 0,y1 : 0,x2 : 0,y2 : 1,stop : 0 #727272,stop : .1 #4c4c4c,stop : .89 #444,stop : 1 #444) +} +QMdiArea QTabBar QToolButton{ + background:qlineargradient(x1 : 0,y1 : 0,x2 : 0,y2 : 1,stop : 0 #878787,stop : .1 #616161,stop : .89 #4f4f4f,stop : 1 #4f4f4f); + border:1px solid #333; + border-radius:0 +} +QMdiArea QTabBar QToolButton::left-arrow{ + image:url(":icons/small_arrow_left-light.png") +} +QMdiArea QTabBar QToolButton::right-arrow{ + image:url(":icons/small_arrow_right-light.png") +} +QMdiArea QTabBar::close-button:selected{ + image:url(":icons/tab_close_btn.png"); + subcontrol-origin:border; + subcontrol-position:right bottom +} +QMdiArea QTabBar::close-button:!selected{ + image:url(":icons/tab_close_nonselected_btn.png") +} +QMdiSubWindow{ + border-style:solid; + background:#616161 +} +QTabBar::tab:selected,QTabBar::tab:hover{ + color:#eee +} +QDockWidget{ + color:#ddd; + font-weight:bold; + titlebar-close-icon:url(":icons/docktitle-close-btn-light.png"); + titlebar-normal-icon:url(":icons/docktitle-normal-btn-light.png") +} +QDockWidget::title{ + background:qlineargradient(x1 : 0,y1 : 0,x2 : 0,y2 : 1,stop : 0 #3b3b3b,stop : 1 #2e2e2e); + padding-top:4px; + padding-right:22px; + font-weight:bold +} +QDockWidget::close-button,QDockWidget::float-button{ + subcontrol-position:top right; + subcontrol-origin:margin; + text-align:center; + icon-size:16px; + width:14px; + position:absolute; + top:0; + bottom:0; + left:0; + right:4px +} +QDockWidget::close-button{ + right:4px +} +QDockWidget::float-button{ + right:18px +} +QMenuBar{ + background:#474747 +} +QMenuBar::item{ + spacing:3px; + padding:3px 5px; + color:#eee; + background:transparent +} +QMenuBar::item:selected,QMenuBar::item:pressed{ + background:#4f9eee +} +QMenu{ + background:#474747; + border:1px solid #2e2e2e +} +QMenu::item{ + background:#474747; + color:#eee +} +QMenu::item:selected{ + background:#616161 +} +QMenu::active{ + background:#616161; + color:#eee +} +QMenu::separator{ + height:1px; + background:#2e2e2e +} +QMenu::disabled,QMenu::item:disabled{ + color:#6e6e6e +} +QListView{ + background-color:#555; + alternate-background-color:#434343 +} +QListView::item{ + height:22px; + color:#e6e6e6 +} +QListView::item:hover{ + background:#6e6e6e +} +QListView::item::active:hover{ + color:#fff +} +QListView::item:selected,QListView::item::active:selected{ + color:#fff; + background:qlineargradient(x1 : 0,y1 : 0,x2 : 0,y2 : 1,stop : 0 #4f9eee,stop : 1 #2084ea); + border:0 +} +QPushButton{ + color:#e6e6e6; + background:#555; + border-color:#141414 +} +QLabel{ + color:#e6e6e6 +} +QLineEdit,QTextEdit{ + color:#e6e6e6; + background:#5a5a5a +} +QLineEdit{ + border:1px solid #3a3a3a; + border-radius:2px; + padding:1px 2px +} +QDMNodeContentWidget{ + background:transparent; +} +QDMNodeContentWidget QFrame{ + background:transparent +} +QDMNodeContentWidget QTextEdit{ + background:#666 +} +QDMNodeContentWidget QLabel{ + color:#e0e0e0 +} +QGraphicsView{ + selection-background-color:#fff +} diff --git a/opencodeblocks/graphics/theme.py b/opencodeblocks/graphics/theme.py new file mode 100644 index 00000000..0633bafc --- /dev/null +++ b/opencodeblocks/graphics/theme.py @@ -0,0 +1,65 @@ +""" +This module defined Theme, a class that +contains the details of the theme +""" + +import json +from PyQt5.Qsci import QsciLexerPython +from PyQt5.QtGui import QColor + +class Theme: + """ Class holding the details of a specific theme""" + + def __init__(self, name: str, json_str: str = "{}"): + """ + Create a new theme + """ + json_obj = json.loads(json_str) + known_properties = { + "comment_color": "#797979", + "string_color": "#CE9178", + "function_color": "#DCDCAA", + "keyword_color": "#569CD6", + "classname_color": "#4EC9B0", + "literal_color": "#7FB347", + "operator_color": "#D8D8D8" + } + for (property_name, property_value) in known_properties.items(): + if property_name in json_obj: + setattr(self, property_name, json_obj[property_name]) + else: + setattr(self, property_name, property_value) + self.name = name + + def apply_to_lexer(self, lexer: QsciLexerPython): + """ Make the given lexer follow the theme """ + lexer.setDefaultPaper(QColor("#1E1E1E")) + lexer.setDefaultColor(QColor("#D4D4D4")) + + string_types = [ + QsciLexerPython.SingleQuotedString, + QsciLexerPython.DoubleQuotedString, + QsciLexerPython.UnclosedString, + QsciLexerPython.SingleQuotedFString, + QsciLexerPython.TripleSingleQuotedString, + QsciLexerPython.TripleDoubleQuotedString, + QsciLexerPython.TripleSingleQuotedFString, + QsciLexerPython.TripleDoubleQuotedFString, + ] + + for string_type in string_types: + lexer.setColor(QColor(self.string_color), string_type) + + lexer.setColor( + QColor( + self.function_color), + QsciLexerPython.FunctionMethodName) + lexer.setColor(QColor(self.keyword_color), QsciLexerPython.Keyword) + lexer.setColor(QColor(self.classname_color), QsciLexerPython.ClassName) + lexer.setColor(QColor(self.literal_color), QsciLexerPython.Number) + lexer.setColor(QColor(self.operator_color), QsciLexerPython.Operator) + lexer.setColor( + QColor( + self.comment_color), + QsciLexerPython.CommentBlock) + lexer.setColor(QColor(self.comment_color), QsciLexerPython.Comment) diff --git a/opencodeblocks/graphics/theme_manager.py b/opencodeblocks/graphics/theme_manager.py new file mode 100644 index 00000000..f366f52d --- /dev/null +++ b/opencodeblocks/graphics/theme_manager.py @@ -0,0 +1,69 @@ +""" +This module provides `theme_manager()`, +a method that returns a handle to the theme manager of the application. + +The theme manager provides the color scheme for the syntax highlighting of the text areas containing code. +""" +import os +from typing import List + +from PyQt5.QtGui import QFontDatabase +from PyQt5.QtCore import pyqtSignal, QObject + +from opencodeblocks.graphics.theme import Theme + +class ThemeManager(QObject): + """ Class loading theme files and providing the options set in those files """ + + themeChanged = pyqtSignal() + + def __init__(self, parent = None): + """ Load the default themes and the fonts available to construct the ThemeManager """ + super().__init__(parent) + self._preferred_fonts = ["Inconsolata", "Roboto Mono", "Courier"] + self.recommended_font_family = "Monospace" + qfd = QFontDatabase() + available_fonts = qfd.families() + for font in self._preferred_fonts: + if font in available_fonts: + self.recommended_font_family = font + break + + self._themes = [] + self._selected_theme_index = 0 + theme_path = "./themes" + theme_paths = os.listdir(theme_path) + for p in theme_paths: + full_path = os.path.join(theme_path, p) + if os.path.isfile(full_path) and full_path.endswith(".theme"): + name = os.path.splitext(os.path.basename(p))[0] + with open(full_path, 'r', encoding="utf-8") as f: + theme = Theme(name, f.read()) + self._themes.append(theme) + @property + def selected_theme_index(self): + return self._selected_theme_index + + @selected_theme_index.setter + def selected_theme_index(self, value: int): + self._selected_theme_index = value + self.themeChanged.emit() + + def list_themes(self) -> List[str]: + """ List the themes """ + return [theme.name for theme in self._themes] + + def current_theme(self) -> Theme: + """ Return the current theme """ + return self._themes[self.selected_theme_index] + + +theme_handle = None + + +def theme_manager(): + """ Retreive the theme manager of the application """ + global theme_handle + if theme_handle is None: + theme_handle = ThemeManager() + return theme_handle diff --git a/opencodeblocks/graphics/window.py b/opencodeblocks/graphics/window.py index 98923246..e8dd41c4 100644 --- a/opencodeblocks/graphics/window.py +++ b/opencodeblocks/graphics/window.py @@ -13,6 +13,7 @@ from opencodeblocks import __appname__ as application_name from opencodeblocks.graphics.view import MODE_EDITING from opencodeblocks.graphics.widget import OCBWidget +from opencodeblocks.graphics.theme_manager import theme_manager from opencodeblocks.graphics.qss import loadStylesheets @@ -43,6 +44,9 @@ def __init__(self): self.windowMapper = QSignalMapper(self) self.windowMapper.mapped[QWidget].connect(self.setActiveSubWindow) + self.themeMapper = QSignalMapper(self) + self.themeMapper.mapped[int].connect(self.setTheme) + # Menus self.createActions() self.createMenus() @@ -156,12 +160,26 @@ def createMenus(self): self.editmenu.addSeparator() self.editmenu.addAction(self.actDel) + self.viewmenu = self.menuBar().addMenu('&View') + self.thememenu = self.viewmenu.addMenu('Theme') + self.thememenu.aboutToShow.connect(self.updateThemeMenu) + self.windowMenu = self.menuBar().addMenu("&Window") self.updateWindowMenu() self.windowMenu.aboutToShow.connect(self.updateWindowMenu) self.menuBar().addSeparator() + def updateThemeMenu(self): + self.thememenu.clear() + theme_names = theme_manager().list_themes() + for i in range(len(theme_names)): + action = self.thememenu.addAction(theme_names[i]) + action.setCheckable(True) + action.setChecked(i == theme_manager().selected_theme_index) + action.triggered.connect(self.themeMapper.map) + self.themeMapper.setMapping(action, i) + def updateWindowMenu(self): self.windowMenu.clear() self.windowMenu.addAction(self.actClose) @@ -346,3 +364,5 @@ def writeSettings(self): def setActiveSubWindow(self, window): if window: self.mdiArea.setActiveSubWindow(window) + def setTheme(self, theme_index): + theme_manager().selected_theme_index = theme_index \ No newline at end of file diff --git a/themes/default.theme b/themes/default.theme new file mode 100644 index 00000000..a525673d --- /dev/null +++ b/themes/default.theme @@ -0,0 +1,9 @@ +{ + "comment_color":"#797979", + "string_color":"#CE9178", + "function_color":"#DCDCAA", + "keyword_color":"#569CD6", + "classname_color":"#4EC9B0", + "literal_color":"#7FB347", + "operator_color":"#D8D8D8" +} \ No newline at end of file diff --git a/themes/monokai.theme b/themes/monokai.theme new file mode 100644 index 00000000..823beea4 --- /dev/null +++ b/themes/monokai.theme @@ -0,0 +1,9 @@ +{ + "comment_color":"#797979", + "string_color":"#e5b567", + "function_color":"#b4d273", + "keyword_color":"#b05279", + "classname_color":"#b4d273", + "literal_color":"#9e86c8", + "operator_color":"#6c99bb" +}