From 27820ec6e6bd46d965640c88035ad959bd00a4ea Mon Sep 17 00:00:00 2001 From: ptsavol <43600314+ptsavol@users.noreply.github.com> Date: Thu, 16 Jan 2025 17:02:20 +0200 Subject: [PATCH] Open default Python or Julia in Basic Console (#3033) - Enables opening the default Python or Julia in the Basic Console without running a Tool first. There are two new buttons in the Main Windows' Consoles menu for this purpose --- CHANGELOG.md | 2 + spinetoolbox/helpers.py | 17 + spinetoolbox/resources_icons_rc.py | 469 ++++++++++++++---- spinetoolbox/ui/mainwindow.py | 18 +- spinetoolbox/ui/mainwindow.ui | 20 + spinetoolbox/ui/resources/julia-logo.svg | 8 + .../ui/resources/menu_icons/terminal.svg | 1 + spinetoolbox/ui/resources/python-logo.svg | 265 ++++++++++ spinetoolbox/ui/resources/resources_icons.qrc | 3 + spinetoolbox/ui_main.py | 76 ++- .../widgets/persistent_console_widget.py | 227 +++++++-- tests/mock_helpers.py | 1 - tests/test_ToolboxUI.py | 93 +++- 13 files changed, 1032 insertions(+), 168 deletions(-) create mode 100644 spinetoolbox/ui/resources/julia-logo.svg create mode 100644 spinetoolbox/ui/resources/menu_icons/terminal.svg create mode 100644 spinetoolbox/ui/resources/python-logo.svg diff --git a/CHANGELOG.md b/CHANGELOG.md index df2a005a5..6de930585 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.1.0/) ### Added - Support for Python 3.13. +- You can now open a detached Basic Console with the default Python or Julia from the Consoles menu. Default +Python and Julia are defined in app settings (File->Settings->Tools) ### Changed diff --git a/spinetoolbox/helpers.py b/spinetoolbox/helpers.py index 0e53fe092..77d788e42 100644 --- a/spinetoolbox/helpers.py +++ b/spinetoolbox/helpers.py @@ -672,6 +672,23 @@ def icon(self, info): return super().icon(info) +def basic_console_icon(language): + """Returns an SVG icon for the given language or an empty QIcon if not available. + + Args: + language (str): Kernel language + + Returns: + QIcon: Icon + """ + if language == "python": + return QIcon(":/symbols/python-logo.svg") + elif language == "julia": + return QIcon(":/symbols/julia-logo.svg") + else: + return QIcon() + + def ensure_window_is_on_screen(window, size): """ Checks if window is on screen and if not, moves and resizes it to make it visible on the primary screen. diff --git a/spinetoolbox/resources_icons_rc.py b/spinetoolbox/resources_icons_rc.py index c2238df72..c41ef068c 100644 --- a/spinetoolbox/resources_icons_rc.py +++ b/spinetoolbox/resources_icons_rc.py @@ -179,6 +179,234 @@ \x0d\x02\xac\x9e\x05\xbe\xb8\x9d\x00\x06\x02\xb0S\xb7\xc4|\ \x98\xbb\xb2?!\x00;\x00\x00\x00\x00\x00\x00\x00\x00\x00\ \ +\x00\x00\x09\x85\ +\x00\ +\x00%\x85x\xda\xedZ[o\xdb\xc8\x15~\xcf\xaf`\ +\x95\x97\x04\x15Gs\xbf(V\xf6!\x8b-\xf6\xa1(\ +\xd0MZ\xa0o\xb48\xb2\xd8P\xa2@\xd2\xb7\xfc\xfa\ +~\xc3\x9bH[\xde\xd8^'\x01\x8a\x15\x11D\ +\xd9a\x22\x87\xdbz[\xec\xe3\xbc\xb8(\xe2b\x9f\xdf\ +\x12\xc4n\x5c\xaf\xb3\xb4\xde\xaefV\x10a\x18e\xe6\ +P7\xe6\xad\xcf.\xb65\x82Q$L\x19\xb5\x9d}\ +H\xc7\xdf\x1c\x8a\xb2\x8e7Y\xee\x1f\x88p\xd8_\x9c\ +\xecr\x93\x1e\xb2\xd5\x8c\x0bN\xa4<\xe9p{\xd7\x01\ +\x8b\xb6\xaf\x96\xbd\xdbj\xf6\x10\xe6!\x91\xea\x90\xac}\ +\xb5\xe8\xed\xa3\xfe=(C\xff\xde@\xaa\xe2\xb2\x5c\xfb\ +\x0d\x86\xf0d\xef\xeb\xc5\xcf\x1f\x7f\x1e\x1acJ\xd2:\ +\x1d\x0ds\x93c\xecI\x0e\xd7\xa2\x89\xce\x9cs\x8b\xa6\ +\xf5\xe8}\xca\x8fSJ\x17=\xfc]jW\x17\x8f\xf4\ +,\xd3\xcd\x83\xb1)_\x80\x06\xf0\x88\xab\xdb}\x9d\xdc\ +\xc4\xfb\xea\xf5\xa8\xebz=\xf4\x5c\x07.gW~]\ +\xecv\xc5\xbej\xc1\x9b8\xa7G\xe7\xc3e\x997\x1e\ +\xe9z\xe1s\xbf\xf3\xfb\xbaZ0\xc2\x16\xb3\xf7\xf0?\ +\xdb\xf9:I\x93:\x09}[Z\xf7\x16\x10\xaaq\x81\ +\x13\x92Z\xfe\xf3\xe7_\xda;\xdc\xaf\xd7\xcb\x7f\x17\xe5\ +\xe7\xee\x16\x9f\xe0\x90\x9c\x17\x97\xa0\xdc\xec\xfd`>K\ +\xd7K,\xcb.\xa9\xdfg\xbb\xe4\xc2\x070\xfe\x8a\x04\ +\xcf\x16\xc7\x86\x89s}{\xf0\xc7A\xdbaK\xdf\xae\ +\xef\xc9\x09\xa5\xeb]\x16:-~\xab\xb3<\xff5\x04\ +\x99E\x8b!\xcfE\x97h7\x8d\xc5h\x1eg\x8b~\ +\x9e\xcd\xdd \xb9@\xc2\xf4*\xf3\xd7\x1d =\xb9\xaf\ +\xb3}Z\x5c\xc7\xbd\xb08\xb5m!\xb8\xef\xd2I\x92\ +9\xa3\xefz\x1c\x90_\xb5M\xe0\x85\x11N5\x16\xa0\ +?&\x14\x0aH?\xfc\xc5e\x96\xfa\xba\xc8}\x99\xec\ +\x03\x0al\xd4T\x22\xd4\xc9\x96\xe2\xfc\xbf~]\x9fn\ +;/\xca\xd4\x97C$v\xa7a]\xe4E\xb9\x9a\xbd\ +\xd6\xcd\xa7k\x0a\xb9\xf5\x0d\x9b\xe63;\x12\xe6<\xa9\ +\xfc\xdd\xc9|)\x8a\x1d\xe6H\x98\xd4L\xcb{s]\ +\xdf\xacf\x8e\x11\xa5\xacv\xf6^#\xb2\x92\x868\xc7\ +\xf5\x03\x10\xdf\x84\x099\xfe@+\xba\x9b\xfb\x11/\xcb\ +\x12\xd4\x8f\xf3\xe4\xd6\x97\xa3\xda=\x94P\xce\xe8n\xd7\ +\x99\xfaU\x96G\xd3\xe5>\xabQ\x0f\x86\xfba\xe0j\ +[\x5c\x7fu]\xd7[\xbf\xfe\xec\xcb\xf3\x22)\x01\xd8\ +=\xe6\xa4\xbe\xfa\xdc\xc3\x9b\xb2p\xdd\xf3(\xd6\x97A\ +\xbaq\x97GW\xcf\xb1W |\xe0\xc1j\xb6I\xf2\ +\xfb\xeb\xd0A\xb2Kn\xb2]\xf6\xc57\xc1[\x81\x9c\ +\xa5~S\x1d\x171\xdc\xc9^\xef\xa8\x82>)\xffV\ +&i\x86\x98\xbd\x22\x83\xdb\xb4\x85\x1b\xa7\x06\xbd\x9fU\ +uq8\xaa\xb7\xaaos\x10/\x18\xe3fn\xcb\xd7\ +\xe76\x5c\xef\x1aS\xc7\xbf%%\xd2Y*\x1c\xe7\xef\ +f\xc7\xce\xc5fS\xf9\xfa\x88T\x1f>tEP3\ +\x16\xf9W\xc3\x9aM\xb8\xee\x84=\x15\x8d\x9d\x8e\xe6\x86\ +hg\x8b\xe9\xfc\x9f\x0c\x975\xb3?\x987Q\x8f\xc7\ +\xc9\xba\xef\x8a\x13{)\x9c\x84F\xe9|\x02\xadx\xb8\ +\x9e\x8d\x13\xa2\xd9'\xe1t.\xc2\xf5L\x9c\x84\xb6\xf4\ +\xc5p\xe2\xe2)8md\xb8\xa6y\xb3\xc7\x82\xc4q\ +\x98{\x02H\xd7\xdb\xac\xf6_\x0f\xc5N\x87z1\x84\ +\xa4\x1e\x0e1\x8fBh\x93Jq~'\xed\xc7\xf2\x08\ +\xb1\xc4\x93x\xb4\xd9xk\xc4\xf3@B0\xf5r \ +\xa1P<>m\x958\xc0\xf4l\x90Fe\xe21\xd1\ +\x04\xd5\xce\xd9GD;\x8d\x92\x13\xcfC\xe9\x86a'\ +\xe7\x92p\xe1\x9c\x1eF\xbe\x85\x95II\x8c2\xcc\x0c\ +\xd6\x1b\xbe\x9a\xc5Z\x11A\xad\xa2\xfc\xe8\xccO9\x9f\ +\xd8\x15\x9c\x1d\x8d\x15\x1e@\x96\xdb\xd2\xe39\xe1\xf5\x09\ +.\xf7~\x17\x9d\xf1S{\x1c\xb8\xac|\xf9[xt\ +\xfa\xc7\xfe\xd3p\x088z}\xc4)\xb0\x0a\x07\xee\xd5\ +\xac\x0e_s<\x0a\xbfa\x94\x12n(\x9f;G4\ +cL\xbf=B\xf50(\xccp\xe2$\x8e\xbf\x13P\ +\x8c!\xd2(g\xc5\x04\x14\xae\x89F\x00g'\x98\x18\ +M\x04\xc3%~\x17\x13G\x1f\x87\x09\xa8\xfbC19\ +\x1e0\x8b<\xc7\x99{5K\xf2\xeb\xe4\xb6zr\xf6\ +'@P#b|\x9b\xc9}\xab\x95\xfd\xa6\xa0\x8dd\ +p\x124\xf7\x1dA{\xd1\x1a\xf1\x03\xa9\xc6\xa5\xfd\x93\ +jO\xa5\x1aW\xf4O\xaa=\x035\xf5\xc7Q\xdb%\ +u\x99\xdd\xbc\xc11_s%\xd9\x9c\x86\x0b7x6\ +\xe4\xf3\x981\xa2\x9c\x91\xf3\xd8\x10M\x9d\x92\xff\xa7\x9a\ +U\xf6\xc7\x02\xf9\x80\x8c5aF\x03\xe4\xe7\xc98\xa4\ +\x98\xe4/\x89c\xf3\x96b\x8c\xe34\x04\xb7\xf4\xc8\xd7\ +\xf0bJc\xd2\xcc\xdaQ\x1d\x0ao\xa4\x98\xe0\x84[\ +e\x8eCmN\xfanN\xfa\x96@\xc0\x11*p\xfc\ +\x15\x8fX\x8e~\x1d\x981Nk|e\xd4\x12#\xc5\ +h\x01~\x7f\xc5\x7f\x9c\xbe\x994\xdf^\xdf\x928\xc7\ +XKP\xb0J\x18v\x87\x99\x8a\x12\x17j\xe8T\xe2\ +\x8e\x13\xa1\x98\xd1\x13fb\x1c,\x0c\x93r\xaapa\ +P\x8c\xf9\xf8\xe5\xcfwW8\xa0\xb4?\x1c\xca \x5c\ +i\x9d0\x13(9EA\x10\xc6\xb2)\x94B\x11\xad\ +\x15WS(\x11C8\xa3\xcd\x0f\x149\x93\x96\xbe\x18\ +\x94\x00J:\xaa\xb4\xf2\xb1\x9d\xc7\xb4\xd9L\xf0\xa8\xa9\ +\xe7\x8cP%\xb5\xb6sA\x8ccJ*\xe3c3\x8f\ +-n)\xb5s&9\x91\x9a\xd3\x11\xc2\xdf\xa5\xdet\ +/|\x17\xe1\x1do\xf3\xed\x90\xd4\xdbW\xe3'\xeeM\ +\x96\xe7\xcb\xcb2\x7f\xf3\xfa>\x03\xdf\xbe\x0b\xad\xf7\xde\ +P\x00\xe2\xbfG\x0a\xeca\xd6X5w\x04\xea2\x92\ +3\x1f\xcb\xe8C\x04\xfd\x09HMpP\x8dr\xce\xb1\ +U\x98H*\xe2\x94\xb1\x12\x05\x8d\xc8P\xf6\xb5\x83\x91\ +\x13F5\xb2\x0f\xe8\x01F<\xb2G\x82\x12\xa3)\x9e\ +\xfc\x01$\x05=\x8d`\x11\xb7\x01D\xcch\x0ea:\ +i\x0c\x93\x83\x0d}\x15&\xcb\x99v\xd1U\xc4\xb0\x22\ +HJE\xdb\x08\xe4\xb5\x8c+\x18\x05\x91\x88\x12lq\ +o\x8c\xc3\x8f\x22\xc1\xb6\x8e\xb0\xbf\x19\xc7\xa5rs\x1a\ +\x05Ih\x060\xad\x9d\xe3\x9b\x85\x22\x0c\x8c:,:\ +>XF\x81MQ`\xfc8,'\xb3\xac1\x86\xa0\ +\x1c\xc5:XU\xf8-V\x85\x15\xd7DY\x0dUE\ +t\xce\x91\xb7\x8a\x18\x01s\x1c\xb7\x02\xd3\xc0 V\xf1\ +H\x13\x10EI`\xd5\x8d,m\x84\x1c\xf8\xddp\xdb\ +\xc8u\x13\xbbB>\x9c4\x99S\xd0\x8bX\xb8R\x1e\ +a\x93Fd\x89\xed\x1aq5\xe4\x1e\x06B\xdej0\ +t\x98\x98f\xfekt\x90J:\x85\x9a\x10\xb1\x0e \ +1\x8f\xb1\x7f\x0b\xcb\xb4\x1cl\xe8.\x88n`\x8c\xb9\ +\x22J\xb0>4\xa6\xa4\xb5\x10\x16\xb3F/\x1e\xd2\x0d\ +\x99\x19\x8e\xf51,\x8a'#\x84\x999\xac\xf8\x87H\ +cv\x16\xfa\xb0\xf3\xf0C\xb9q\x12\xe5!R\x8e\xe0\ +\x98%E#)\xcaQ]\x1c\x15\x0f1\xec?\xd1\xae\ +Y(5\xb7-&\x149\x19\x87\xac\x10]\xe3\xa4b\ +0\xa7\xc0\x09\x98\xe7\x80\xcaY-\x95\x1e,\x0a\xc9\x06\ +x\xc3j\xf9X\xc3\x03\x13\x16\xa2Y<\xae\x99\xa1\x02\ +.\xb4]\xe4c\x1f\xda\xaf:vei4&\x85\xbe\ +G\x87\xd0\xd7\x18\xc9F}\xe2\xa1\x13\xa3M)@ \ +\xae\x85\x12QX>\xec\xee2\xb8\xf4\xb9\x8cz\xf5\xa6\ +/\xa3\xdf\xd9\x82j\x99\x93\xfd\xcb\xea'\xa9X=\xac\ +\xe2]dQ\xb5\x91$d\x04A\x05\x0a\xb52\x0aT\ +\xd5\xddJ\x83y\x82\x1a\xa5\x82Rlx\xc6V\x81\xdb\ +\xae\xf9\xb4\xe2\x80\xc1t\xea\xea\xe9\x05_\x14\x01+D\ +\xa3\xaa\x96\x0a\xd2\xcd5`\xb2\xd2\x8a\x11=\x06~u\ +\xf4\x92\xa6\x89\xda\xd2K\xca(\x1c\x13m\x10d8\x90\ +J(}D\xce\xb6/\xa52\xb2DZ#\x04\xc3r\ +\x82\xd5\x9a\x05\xf2k\xae\xb1\xa7\xcd\xc3A\x15C\x0d\xe4\ +GF!g\xa84p\x13e\x09\x14\x1ck@\x11k\ +M\x18\xe1\x8e\x04B\x98\x7fE\x16\xb2\xa6\xd417\x99\ +0\xd4q\xac2\x83\xb5\x1fA6z\x0bUF7z\ +\xc3\x06\xea`f!\x96\x14\x98\x94\xed]\xb1G\xc7\x83\ +\xe8A6\xe7\x80 T.\x1c\x0a\xbf\x86E[\x94\xd4\ +F\xd3XXctPb[]\xc2T\xa0\x03\x1b\x94\ +\x89&)Y\xe0\x22vjkFc\xc6\xa7\x02}i\ +\x14\xa5\x9a\x8a8\x87@\xdbr\xd1\xe8)\xb0\x1d{Z\ + \xfbHR\x81\xea&\x9a\xaa\x03\xeb6\x08*\xf0\x5c\ +6\x82\x0aDo5\xd7\xe0\x17\x8fU\xd8ZZ\xd1\x86\ +U\x99\xa8\x09\xaa\x9d\x8a\xa9\xf3oB\xc4\x9dh\xa3F\ +\xb2\xd8Jz\xb5I\x13\xdd\xd1\x1f,\xf7\x85\xa4\x86_\ +B}\x9eg\x87\xcaO\xb44\xfa}\x12%\x89\xbf;\ +j\xeb\xfe\xc1\xe2\xae\xb6\xda\xdb\xf22\xf7\xcb}\xb1\xff\ +\xe2\xcb\xe2]U\x97\xc5\xe7\xe6\xd6w\xdf\xdb\xbf\x13X\ +\x02q\xc0({\xe3.\xab}\x99g\xf8o9\xd8\xd2\ +\xa4\xda&e\x99\xdcN\xba\xdfU\xf201\xeb\xfa\x93\ +l8\x5c(\x15P\x02\x95{[8.\x80,\xd87\ +M\x7f\xf6,\xe1\x87\x93\x9b\x13\xd4\x99\xfe\x98W\xc2\x0f\ +\x02\xd7\xd8zY{\x06>\x0b\x7f\xbe\xf1\xfe\xd5\xff\x00\ +\x0d\x9d\xd5s\ +\x00\x00\x04m\ +<\ +?xml version=\x221.\ +0\x22 encoding=\x22UTF\ +-8\x22?>\x0a\x0a\x0a\x0a\x0a\x0a\ +\x0a\x0a\ \x00\x00\x06U\ \x89\ PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ @@ -31243,6 +31471,39 @@ 6.207 9.997-36.2\ 04-.001z\x22/>\ +\x00\x00\x01\xe1\ +<\ +svg xmlns=\x22http:\ +//www.w3.org/200\ +0/svg\x22 viewBox=\x22\ +0 0 640 512\x22>\ +\ \x00\x00\x0b\xf3\ <\ ?xml version=\x221.\ @@ -32024,6 +32285,14 @@ \x0b\xac\x876\ \x00a\ \x00j\x00a\x00x\x00-\x00l\x00o\x00a\x00d\x00e\x00r\x00.\x00g\x00i\x00f\ +\x00\x0f\ +\x00O\x92\x87\ +\x00p\ +\x00y\x00t\x00h\x00o\x00n\x00-\x00l\x00o\x00g\x00o\x00.\x00s\x00v\x00g\ +\x00\x0e\ +\x0d\xc0\xc6\xc7\ +\x00j\ +\x00u\x00l\x00i\x00a\x00-\x00l\x00o\x00g\x00o\x00.\x00s\x00v\x00g\ \x00\x10\ \x00$\x93'\ \x00S\ @@ -32353,6 +32622,10 @@ \x00c\ \x00h\x00e\x00c\x00k\x00.\x00s\x00v\x00g\ \x00\x0c\ +\x0b&r\xc7\ +\x00t\ +\x00e\x00r\x00m\x00i\x00n\x00a\x00l\x00.\x00s\x00v\x00g\ +\x00\x0c\ \x0c\x16L\x07\ \x00c\ \x00u\x00b\x00e\x00_\x00p\x00e\x00n\x00.\x00s\x00v\x00g\ @@ -32399,199 +32672,205 @@ qt_resource_struct = b"\ \x00\x00\x00\x00\x00\x02\x00\x00\x00\x04\x00\x00\x00\x01\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x00\x10\x00\x02\x00\x00\x00\x03\x00\x00\x00_\ +\x00\x00\x00\x10\x00\x02\x00\x00\x00\x03\x00\x00\x00b\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x00\x00\x00\x02\x00\x00\x00\x1a\x00\x00\x00\x09\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00\x1a\x00\x00\x00\x0b\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x00 \x00\x02\x00\x00\x00\x03\x00\x00\x00\x06\ +\x00\x00\x00 \x00\x02\x00\x00\x00\x05\x00\x00\x00\x06\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x004\x00\x02\x00\x00\x00\x01\x00\x00\x00\x05\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x00T\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ \x00\x00\x01\x8f\xec\xcf\x12\xea\ -\x00\x00\x00x\x00\x00\x00\x00\x00\x01\x00\x00\x09\xf5\ +\x00\x00\x00\xbe\x00\x00\x00\x00\x00\x01\x00\x00\x17\xef\ \x00\x00\x01\x8f\xec\xcf\x12\xea\ -\x00\x00\x00\xb2\x00\x00\x00\x00\x00\x01\x00\x008\x89\ +\x00\x00\x00x\x00\x01\x00\x00\x00\x01\x00\x00\x09\xf5\ +\x00\x00\x01\x8f\xee\x1c\xe1_\ +\x00\x00\x00\xf8\x00\x00\x00\x00\x00\x01\x00\x00F\x83\ \x00\x00\x01\x8f\xec\xcf\x13\x09\ -\x00\x00\x00\x9e\x00\x01\x00\x00\x00\x01\x00\x00\x10N\ +\x00\x00\x00\xe4\x00\x01\x00\x00\x00\x01\x00\x00\x1eH\ \x00\x00\x01\x8f\xec\xcf\x12\xea\ -\x00\x00\x02R\x00\x00\x00\x00\x00\x01\x00\x06A\x9c\ +\x00\x00\x00\x9c\x00\x00\x00\x00\x00\x01\x00\x00\x13~\ +\x00\x00\x01\x93\x87\xeb\x09\xf1\ +\x00\x00\x02\x98\x00\x00\x00\x00\x00\x01\x00\x06O\x96\ \x00\x00\x01\x8f\xec\xcf\x12\xea\ -\x00\x00\x01\xea\x00\x00\x00\x00\x00\x01\x00\x062\x92\ +\x00\x00\x020\x00\x00\x00\x00\x00\x01\x00\x06@\x8c\ \x00\x00\x01\x8f\xec\xcf\x13\x09\ -\x00\x00\x04p\x00\x00\x00\x00\x00\x01\x00\x06\xa8\xcd\ +\x00\x00\x04\xb6\x00\x00\x00\x00\x00\x01\x00\x06\xb6\xc7\ \x00\x00\x01\x8f\xec\xcf\x12\xea\ -\x00\x00\x04\xb8\x00\x00\x00\x00\x00\x01\x00\x06\xbd\xb7\ +\x00\x00\x04\xfe\x00\x00\x00\x00\x00\x01\x00\x06\xcb\xb1\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x03D\x00\x00\x00\x00\x00\x01\x00\x06\x88\x1a\ +\x00\x00\x03\x8a\x00\x00\x00\x00\x00\x01\x00\x06\x96\x14\ \x00\x00\x01\x8f\xec\xcf\x13\x09\ -\x00\x00\x04\x98\x00\x00\x00\x00\x00\x01\x00\x06\xaa\xa1\ +\x00\x00\x04\xde\x00\x00\x00\x00\x00\x01\x00\x06\xb8\x9b\ \x00\x00\x01\x8f\xec\xcf\x12\xea\ -\x00\x00\x04\x0c\x00\x00\x00\x00\x00\x01\x00\x06\x99\xa7\ +\x00\x00\x04R\x00\x00\x00\x00\x00\x01\x00\x06\xa7\xa1\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x02\x80\x00\x00\x00\x00\x00\x01\x00\x06E\xb2\ +\x00\x00\x02\xc6\x00\x00\x00\x00\x00\x01\x00\x06S\xac\ \x00\x00\x01\x8f\xec\xcf\x12\xea\ -\x00\x00\x02h\x00\x00\x00\x00\x00\x01\x00\x06B\xab\ +\x00\x00\x02\xae\x00\x00\x00\x00\x00\x01\x00\x06P\xa5\ \x00\x00\x01\x8f\xec\xcf\x13\x09\ -\x00\x00\x03\xf6\x00\x00\x00\x00\x00\x01\x00\x06\x978\ +\x00\x00\x04<\x00\x00\x00\x00\x00\x01\x00\x06\xa52\ \x00\x00\x01\x8f\xec\xcf\x12\xea\ -\x00\x00\x03^\x00\x00\x00\x00\x00\x01\x00\x06\x8a3\ +\x00\x00\x03\xa4\x00\x00\x00\x00\x00\x01\x00\x06\x98-\ \x00\x00\x01\x8f\xec\xcf\x12\xea\ -\x00\x00\x02\xc4\x00\x00\x00\x00\x00\x01\x00\x06Q\xfb\ +\x00\x00\x03\x0a\x00\x00\x00\x00\x00\x01\x00\x06_\xf5\ \x00\x00\x01\x8f\xec\xcf\x12\xea\ -\x00\x00\x02.\x00\x00\x00\x00\x00\x01\x00\x06?\xec\ +\x00\x00\x02t\x00\x00\x00\x00\x00\x01\x00\x06M\xe6\ \x00\x00\x01\x8f\xec\xcf\x12\xea\ -\x00\x00\x03(\x00\x00\x00\x00\x00\x01\x00\x06b\xb3\ +\x00\x00\x03n\x00\x00\x00\x00\x00\x01\x00\x06p\xad\ \x00\x00\x01\x8f\xec\xcf\x12\xea\ -\x00\x00\x01\xbe\x00\x00\x00\x00\x00\x01\x00\x060\xaf\ +\x00\x00\x02\x04\x00\x00\x00\x00\x00\x01\x00\x06>\xa9\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x03\x98\x00\x00\x00\x00\x00\x01\x00\x06\x94o\ +\x00\x00\x03\xde\x00\x00\x00\x00\x00\x01\x00\x06\xa2i\ \x00\x00\x01\x8f\xec\xcf\x13\x09\ -\x00\x00\x01\xa6\x00\x00\x00\x00\x00\x01\x00\x06/\x9c\ +\x00\x00\x01\xec\x00\x00\x00\x00\x00\x01\x00\x06=\x96\ \x00\x00\x01\x8f\xec\xcf\x13\x09\ -\x00\x00\x04$\x00\x00\x00\x00\x00\x01\x00\x06\xa0\x10\ +\x00\x00\x04j\x00\x00\x00\x00\x00\x01\x00\x06\xae\x0a\ \x00\x00\x01\x8f\xec\xcf\x13\x09\ -\x00\x00\x03\xb0\x00\x02\x00\x00\x00\x09\x00\x00\x00V\ +\x00\x00\x03\xf6\x00\x02\x00\x00\x00\x09\x00\x00\x00Y\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x03~\x00\x02\x00\x00\x003\x00\x00\x00#\ +\x00\x00\x03\xc4\x00\x02\x00\x00\x004\x00\x00\x00%\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x02\x0a\x00\x00\x00\x00\x00\x01\x00\x065G\ +\x00\x00\x02P\x00\x00\x00\x00\x00\x01\x00\x06CA\ \x00\x00\x01\x8f\xec\xcf\x13\x09\ -\x00\x00\x04T\x00\x00\x00\x00\x00\x01\x00\x06\xa3)\ +\x00\x00\x04\x9a\x00\x00\x00\x00\x00\x01\x00\x06\xb1#\ \x00\x00\x01\x8f\xec\xcf\x13\x09\ -\x00\x00\x03\xda\x00\x00\x00\x00\x00\x01\x00\x06\x96\x00\ +\x00\x00\x04 \x00\x00\x00\x00\x00\x01\x00\x06\xa3\xfa\ \x00\x00\x01\x8f\xec\xcf\x12\xea\ -\x00\x00\x02\xda\x00\x00\x00\x00\x00\x01\x00\x06T\x05\ +\x00\x00\x03 \x00\x00\x00\x00\x00\x01\x00\x06a\xff\ \x00\x00\x01\x8f\xec\xcf\x12\xea\ -\x00\x00\x02\x9e\x00\x00\x00\x00\x00\x01\x00\x06G\x93\ +\x00\x00\x02\xe4\x00\x00\x00\x00\x00\x01\x00\x06U\x8d\ \x00\x00\x01\x8f\xec\xcf\x13\x09\ -\x00\x00\x03\x02\x00\x00\x00\x00\x00\x01\x00\x06`G\ +\x00\x00\x03H\x00\x00\x00\x00\x00\x01\x00\x06nA\ \x00\x00\x01\x8f\xec\xcf\x12\xea\ -\x00\x00\x02R\x00\x00\x00\x00\x00\x01\x00\x07\x0a\xf8\ +\x00\x00\x02\x98\x00\x00\x00\x00\x00\x01\x00\x07\x18\xf2\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x0b\xec\x00\x00\x00\x00\x00\x01\x00\x07\xbf\xb6\ +\x00\x00\x0cP\x00\x00\x00\x00\x00\x01\x00\x07\xcf\x95\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x060\x00\x00\x00\x00\x00\x01\x00\x06\xec\xdf\ +\x00\x00\x06v\x00\x00\x00\x00\x00\x01\x00\x06\xfa\xd9\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x09\x14\x00\x00\x00\x00\x00\x01\x00\x07Hx\ +\x00\x00\x09Z\x00\x00\x00\x00\x00\x01\x00\x07Vr\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x09\x9a\x00\x00\x00\x00\x00\x01\x00\x07_J\ +\x00\x00\x09\xe0\x00\x00\x00\x00\x00\x01\x00\x07mD\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x06\xf6\x00\x00\x00\x00\x00\x01\x00\x07\x08\x9c\ +\x00\x00\x07<\x00\x00\x00\x00\x00\x01\x00\x07\x16\x96\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x08\x98\x00\x00\x00\x00\x00\x01\x00\x07B\xb4\ +\x00\x00\x08\xde\x00\x00\x00\x00\x00\x01\x00\x07P\xae\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x09v\x00\x00\x00\x00\x00\x01\x00\x07]\x88\ +\x00\x00\x09\xbc\x00\x00\x00\x00\x00\x01\x00\x07k\x82\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x0a\xce\x00\x00\x00\x00\x00\x01\x00\x07\xa0\x8e\ +\x00\x00\x0b2\x00\x00\x00\x00\x00\x01\x00\x07\xb0m\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x07\xa0\x00\x00\x00\x00\x00\x01\x00\x07\x1ez\ +\x00\x00\x07\xe6\x00\x00\x00\x00\x00\x01\x00\x07,t\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x07\x16\x00\x00\x00\x00\x00\x01\x00\x07\x0e8\ +\x00\x00\x07\x5c\x00\x00\x00\x00\x00\x01\x00\x07\x1c2\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x094\x00\x00\x00\x00\x00\x01\x00\x07ZV\ +\x00\x00\x09z\x00\x00\x00\x00\x00\x01\x00\x07hP\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x08x\x00\x00\x00\x00\x00\x01\x00\x07)\xa4\ +\x00\x00\x08\xbe\x00\x00\x00\x00\x00\x01\x00\x077\x9e\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x08\xba\x00\x00\x00\x00\x00\x01\x00\x07D\xc1\ +\x00\x00\x09\x00\x00\x00\x00\x00\x00\x01\x00\x07R\xbb\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x07\xf2\x00\x00\x00\x00\x00\x01\x00\x07$u\ +\x00\x00\x088\x00\x00\x00\x00\x00\x01\x00\x072o\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x08\xee\x00\x00\x00\x00\x00\x01\x00\x07F\x1d\ +\x00\x00\x094\x00\x00\x00\x00\x00\x01\x00\x07T\x17\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x06V\x00\x00\x00\x00\x00\x01\x00\x06\xee\xe8\ +\x00\x00\x06\x9c\x00\x00\x00\x00\x00\x01\x00\x06\xfc\xe2\ \x00\x00\x01\x8f\xec\xcf\x12\xea\ -\x00\x00\x0a\x00\x00\x00\x00\x00\x00\x01\x00\x07~\x92\ +\x00\x00\x0aF\x00\x00\x00\x00\x00\x01\x00\x07\x8c\x8c\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x07\xb8\x00\x00\x00\x00\x00\x01\x00\x07 m\ +\x00\x00\x07\xfe\x00\x00\x00\x00\x00\x01\x00\x07.g\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x0b>\x00\x00\x00\x00\x00\x01\x00\x07\xa6\xd3\ +\x00\x00\x0b\xa2\x00\x00\x00\x00\x00\x01\x00\x07\xb6\xb2\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x06\x1a\x00\x00\x00\x00\x00\x01\x00\x06\xeap\ +\x00\x00\x06`\x00\x00\x00\x00\x00\x01\x00\x06\xf8j\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x06\xd4\x00\x00\x00\x00\x00\x01\x00\x06\xfa\xbf\ +\x00\x00\x07\x1a\x00\x00\x00\x00\x00\x01\x00\x07\x08\xb9\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x08T\x00\x00\x00\x00\x00\x01\x00\x07(B\ +\x00\x00\x08\x9a\x00\x00\x00\x00\x00\x01\x00\x076<\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x07X\x00\x00\x00\x00\x00\x01\x00\x07\x1a]\ +\x00\x00\x07\x9e\x00\x00\x00\x00\x00\x01\x00\x07(W\ \x00\x00\x01\x8f\xec\xcf\x12\xea\ -\x00\x00\x0b\xa4\x00\x00\x00\x00\x00\x01\x00\x07\xad\xdf\ +\x00\x00\x0c\x08\x00\x00\x00\x00\x00\x01\x00\x07\xbd\xbe\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x0b\x0e\x00\x00\x00\x00\x00\x01\x00\x07\xa5\xce\ +\x00\x00\x0br\x00\x00\x00\x00\x00\x01\x00\x07\xb5\xad\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x05\xfe\x00\x00\x00\x00\x00\x01\x00\x06\xe7K\ +\x00\x00\x06D\x00\x00\x00\x00\x00\x01\x00\x06\xf5E\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x07\xda\x00\x00\x00\x00\x00\x01\x00\x07!\x9a\ +\x00\x00\x08 \x00\x00\x00\x00\x00\x01\x00\x07/\x94\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x09\xd2\x00\x00\x00\x00\x00\x01\x00\x07y\xdc\ +\x00\x00\x0a\x18\x00\x00\x00\x00\x00\x01\x00\x07\x87\xd6\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x0b\xcc\x00\x00\x00\x00\x00\x01\x00\x07\xb0:\ +\x00\x00\x0c0\x00\x00\x00\x00\x00\x01\x00\x07\xc0\x19\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x07:\x00\x01\x00\x00\x00\x01\x00\x07\x18\xeb\ +\x00\x00\x07\x80\x00\x01\x00\x00\x00\x01\x00\x07&\xe5\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x0aT\x00\x00\x00\x00\x00\x01\x00\x07\x8e%\ +\x00\x00\x0a\x9a\x00\x00\x00\x00\x00\x01\x00\x07\x9c\x1f\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x09^\x00\x00\x00\x00\x00\x01\x00\x07\x5ci\ +\x00\x00\x09\xa4\x00\x00\x00\x00\x00\x01\x00\x07jc\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x09\xbc\x00\x00\x00\x00\x00\x01\x00\x07m\xb8\ +\x00\x00\x0a\x02\x00\x00\x00\x00\x00\x01\x00\x07{\xb2\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x06\xc0\x00\x00\x00\x00\x00\x01\x00\x06\xf6%\ +\x00\x00\x07\x06\x00\x00\x00\x00\x00\x01\x00\x07\x04\x1f\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x0a\xf2\x00\x00\x00\x00\x00\x01\x00\x07\xa2T\ +\x00\x00\x0bV\x00\x00\x00\x00\x00\x01\x00\x07\xb23\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x0a\x80\x00\x00\x00\x00\x00\x01\x00\x07\x90\xe7\ +\x00\x00\x0a\xc6\x00\x00\x00\x00\x00\x01\x00\x07\x9e\xe1\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x0a$\x00\x00\x00\x00\x00\x01\x00\x07\x8c\x8f\ +\x00\x00\x0aj\x00\x00\x00\x00\x00\x01\x00\x07\x9a\x89\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x09\xe8\x00\x00\x00\x00\x00\x01\x00\x07{\x8f\ +\x00\x00\x0a.\x00\x00\x00\x00\x00\x01\x00\x07\x89\x89\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x07\x8a\x00\x00\x00\x00\x00\x01\x00\x07\x1c+\ +\x00\x00\x07\xd0\x00\x00\x00\x00\x00\x01\x00\x07*%\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x0a\x98\x00\x00\x00\x00\x00\x01\x00\x07\x931\ +\x00\x00\x0a\xf6\x00\x00\x00\x00\x00\x01\x00\x07\xa2\x91\ +\x00\x00\x01f\xd4x\xbb0\ +\x00\x00\x0a\xde\x00\x00\x00\x00\x00\x01\x00\x07\xa1+\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x06\xaa\x00\x00\x00\x00\x00\x01\x00\x06\xf4{\ +\x00\x00\x06\xf0\x00\x00\x00\x00\x00\x01\x00\x07\x02u\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x0a\xb0\x00\x00\x00\x00\x00\x01\x00\x07\x94\x97\ +\x00\x00\x0b\x14\x00\x00\x00\x00\x00\x01\x00\x07\xa4v\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x0b\x88\x00\x00\x00\x00\x00\x01\x00\x07\xaa\x95\ +\x00\x00\x0b\xec\x00\x00\x00\x00\x00\x01\x00\x07\xbat\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x08&\x00\x01\x00\x00\x00\x01\x00\x07%\xca\ +\x00\x00\x08l\x00\x01\x00\x00\x00\x01\x00\x073\xc4\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x08@\x00\x00\x00\x00\x00\x01\x00\x07&\xa4\ +\x00\x00\x08\x86\x00\x00\x00\x00\x00\x01\x00\x074\x9e\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x0bb\x00\x00\x00\x00\x00\x01\x00\x07\xa8\xb6\ +\x00\x00\x0b\xc6\x00\x00\x00\x00\x00\x01\x00\x07\xb8\x95\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x06\x86\x00\x00\x00\x00\x00\x01\x00\x06\xf0\xb4\ +\x00\x00\x06\xcc\x00\x00\x00\x00\x00\x01\x00\x06\xfe\xae\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x04\xf0\x00\x00\x00\x00\x00\x01\x00\x07\x0c\x07\ +\x00\x00\x056\x00\x00\x00\x00\x00\x01\x00\x07\x1a\x01\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x05X\x00\x00\x00\x00\x00\x01\x00\x078\x22\ +\x00\x00\x05\x9e\x00\x00\x00\x00\x00\x01\x00\x07F\x1c\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x05\x84\x00\x00\x00\x00\x00\x01\x00\x07o\x0c\ +\x00\x00\x05\xca\x00\x00\x00\x00\x00\x01\x00\x07}\x06\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x05\xb0\x00\x00\x00\x00\x00\x01\x00\x06\xe3:\ +\x00\x00\x05\xf6\x00\x00\x00\x00\x00\x01\x00\x06\xf14\ \x00\x00\x01\x8f\xec\xcf\x13\x09\ -\x00\x00\x05\x0e\x00\x00\x00\x00\x00\x01\x00\x06\xca\xc1\ +\x00\x00\x05T\x00\x00\x00\x00\x00\x01\x00\x06\xd8\xbb\ \x00\x00\x01\x8f\xec\xcf\x13\x09\ -\x00\x00\x02\x80\x00\x00\x00\x00\x00\x01\x00\x06\xc6\xaf\ +\x00\x00\x02\xc6\x00\x00\x00\x00\x00\x01\x00\x06\xd4\xa9\ \x00\x00\x01\x8f\xec\xcf\x13\x09\ -\x00\x00\x04\xce\x00\x00\x00\x00\x00\x01\x00\x06\xc4\x9c\ +\x00\x00\x05\x14\x00\x00\x00\x00\x00\x01\x00\x06\xd2\x96\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x05\xe2\x00\x00\x00\x00\x00\x01\x00\x06\xe5\x0c\ +\x00\x00\x06(\x00\x00\x00\x00\x00\x01\x00\x06\xf3\x06\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x05(\x00\x00\x00\x00\x00\x01\x00\x06\xcdK\ +\x00\x00\x05n\x00\x00\x00\x00\x00\x01\x00\x06\xdbE\ \x00\x00\x01\x8f\xec\xcf\x13\x09\ -\x00\x00\x04\xf0\x00\x00\x00\x00\x00\x01\x00\x06\xc8\x90\ +\x00\x00\x056\x00\x00\x00\x00\x00\x01\x00\x06\xd6\x8a\ \x00\x00\x01\x8f\xec\xcf\x13\x09\ -\x00\x00\x05X\x00\x00\x00\x00\x00\x01\x00\x06\xcf\xfe\ +\x00\x00\x05\x9e\x00\x00\x00\x00\x00\x01\x00\x06\xdd\xf8\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x05\x84\x00\x00\x00\x00\x00\x01\x00\x06\xd9\xb8\ +\x00\x00\x05\xca\x00\x00\x00\x00\x00\x01\x00\x06\xe7\xb2\ \x00\x00\x01\x8f\xec\xcf\x12\xfa\ -\x00\x00\x01&\x00\x01\x00\x00\x00\x01\x00\x03\x04\x82\ +\x00\x00\x01l\x00\x01\x00\x00\x00\x01\x00\x03\x12|\ \x00\x00\x01\x8f\xec\xcf\x12\xea\ -\x00\x00\x00\xe6\x00\x01\x00\x00\x00\x01\x00\x02\xbaN\ +\x00\x00\x01,\x00\x01\x00\x00\x00\x01\x00\x02\xc8H\ \x00\x00\x01\x8f\xec\xcf\x12\xea\ -\x00\x00\x01d\x00\x00\x00\x00\x00\x01\x00\x03\x10\xa8\ +\x00\x00\x01\xaa\x00\x00\x00\x00\x00\x01\x00\x03\x1e\xa2\ \x00\x00\x01\x8f\xec\xcf\x12\xea\ " diff --git a/spinetoolbox/ui/mainwindow.py b/spinetoolbox/ui/mainwindow.py index 1e716bc71..09a77ff2a 100644 --- a/spinetoolbox/ui/mainwindow.py +++ b/spinetoolbox/ui/mainwindow.py @@ -192,6 +192,14 @@ def setupUi(self, MainWindow): icon18 = QIcon() icon18.addFile(u":/icons/menu_icons/github-mark.svg", QSize(), QIcon.Mode.Normal, QIcon.State.Off) self.actionGitHub.setIcon(icon18) + self.actionStart_default_python_in_basic_console = QAction(MainWindow) + self.actionStart_default_python_in_basic_console.setObjectName(u"actionStart_default_python_in_basic_console") + icon19 = QIcon() + icon19.addFile(u":/icons/menu_icons/terminal.svg", QSize(), QIcon.Mode.Normal, QIcon.State.Off) + self.actionStart_default_python_in_basic_console.setIcon(icon19) + self.actionStart_default_julia_in_basic_console = QAction(MainWindow) + self.actionStart_default_julia_in_basic_console.setObjectName(u"actionStart_default_julia_in_basic_console") + self.actionStart_default_julia_in_basic_console.setIcon(icon19) self.centralwidget = QWidget(MainWindow) self.centralwidget.setObjectName(u"centralwidget") MainWindow.setCentralWidget(self.centralwidget) @@ -256,9 +264,9 @@ def setupUi(self, MainWindow): self.toolButton_executions = QToolButton(self.dockWidgetContents) self.toolButton_executions.setObjectName(u"toolButton_executions") - icon19 = QIcon() - icon19.addFile(u":/icons/check-circle.svg", QSize(), QIcon.Mode.Normal, QIcon.State.Off) - self.toolButton_executions.setIcon(icon19) + icon20 = QIcon() + icon20.addFile(u":/icons/check-circle.svg", QSize(), QIcon.Mode.Normal, QIcon.State.Off) + self.toolButton_executions.setIcon(icon20) self.toolButton_executions.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup) self.toolButton_executions.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon) @@ -402,6 +410,8 @@ def setupUi(self, MainWindow): self.menuPlugins.addSeparator() self.menuPlugins.addAction(self.actionCreate_plugin) self.menuConsoles.addAction(self.actionStart_jupyter_console) + self.menuConsoles.addAction(self.actionStart_default_python_in_basic_console) + self.menuConsoles.addAction(self.actionStart_default_julia_in_basic_console) self.menuServer.addAction(self.actionRetrieve_project) self.retranslateUi(MainWindow) @@ -610,6 +620,8 @@ def retranslateUi(self, MainWindow): #if QT_CONFIG(tooltip) self.actionGitHub.setToolTip(QCoreApplication.translate("MainWindow", u"

Open Spine-Toolbox repository in GitHub

", None)) #endif // QT_CONFIG(tooltip) + self.actionStart_default_python_in_basic_console.setText(QCoreApplication.translate("MainWindow", u"Start Default Python in Basic Console", None)) + self.actionStart_default_julia_in_basic_console.setText(QCoreApplication.translate("MainWindow", u"Start Default Julia in Basic Console", None)) self.menuFile.setTitle(QCoreApplication.translate("MainWindow", u"&File", None)) self.menuHelp.setTitle(QCoreApplication.translate("MainWindow", u"&Help", None)) self.menuEdit.setTitle(QCoreApplication.translate("MainWindow", u"&Edit", None)) diff --git a/spinetoolbox/ui/mainwindow.ui b/spinetoolbox/ui/mainwindow.ui index 6563a0165..f34080b10 100644 --- a/spinetoolbox/ui/mainwindow.ui +++ b/spinetoolbox/ui/mainwindow.ui @@ -120,6 +120,8 @@ &Consoles + + @@ -885,6 +887,24 @@ <html><head/><body><p>Open Spine-Toolbox repository in GitHub</p></body></html> + + + + :/icons/menu_icons/terminal.svg:/icons/menu_icons/terminal.svg + + + Start Default Python in Basic Console + + + + + + :/icons/menu_icons/terminal.svg:/icons/menu_icons/terminal.svg + + + Start Default Julia in Basic Console + + diff --git a/spinetoolbox/ui/resources/julia-logo.svg b/spinetoolbox/ui/resources/julia-logo.svg new file mode 100644 index 000000000..ed7f17bb3 --- /dev/null +++ b/spinetoolbox/ui/resources/julia-logo.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/spinetoolbox/ui/resources/menu_icons/terminal.svg b/spinetoolbox/ui/resources/menu_icons/terminal.svg new file mode 100644 index 000000000..85b2b5e85 --- /dev/null +++ b/spinetoolbox/ui/resources/menu_icons/terminal.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/spinetoolbox/ui/resources/python-logo.svg b/spinetoolbox/ui/resources/python-logo.svg new file mode 100644 index 000000000..467b07b26 --- /dev/null +++ b/spinetoolbox/ui/resources/python-logo.svg @@ -0,0 +1,265 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spinetoolbox/ui/resources/resources_icons.qrc b/spinetoolbox/ui/resources/resources_icons.qrc index 6e52c62b3..a30b6430e 100644 --- a/spinetoolbox/ui/resources/resources_icons.qrc +++ b/spinetoolbox/ui/resources/resources_icons.qrc @@ -1,6 +1,8 @@ Spine_symbol.png + julia-logo.svg + python-logo.svg spinetoolbox_on_wht.png app.ico @@ -13,6 +15,7 @@ fontawesome5-searchterms.json + menu_icons/terminal.svg menu_icons/bolt-lightning.svg share.svg menu_icons/server.svg diff --git a/spinetoolbox/ui_main.py b/spinetoolbox/ui_main.py index bff0761c2..3e9b3881e 100644 --- a/spinetoolbox/ui_main.py +++ b/spinetoolbox/ui_main.py @@ -17,6 +17,7 @@ import os import pathlib import sys +import threading from zipfile import ZipFile import numpy as np from PySide6.QtCore import QByteArray, QEvent, QMimeData, QModelIndex, QPoint, QSettings, Qt, QUrl, Signal, Slot @@ -43,13 +44,13 @@ QMainWindow, QMenu, QMessageBox, - QScrollArea, QStyleFactory, QToolButton, - QVBoxLayout, QWidget, ) +from spine_engine.spine_engine import _set_resource_limits from spine_engine.load_project_items import load_item_specification_factories +from spine_engine.utils.helpers import resolve_python_interpreter, resolve_julia_executable, resolve_julia_project from spinetoolbox.server.engine_client import ClientSecurityModel, EngineClient, RemoteEngineInitFailed from .config import DEFAULT_WORK_DIR, MAINWINDOW_SS, ONLINE_DOCUMENTATION_URL, SPINE_TOOLBOX_REPO_URL from .helpers import ( @@ -70,6 +71,7 @@ supported_img_formats, unique_name, clear_qsettings, + basic_console_icon, ) from .kernel_fetcher import KernelFetcher from .link import JUMP_COLOR, LINK_COLOR, JumpLink, Link @@ -104,7 +106,7 @@ from .widgets.link_properties_widget import LinkPropertiesWidget from .widgets.multi_tab_spec_editor import MultiTabSpecEditor from .widgets.open_project_dialog import OpenProjectDialog -from .widgets.persistent_console_widget import PersistentConsoleWidget +from .widgets.persistent_console_widget import PersistentConsoleWidget, ConsoleWindow from .widgets.set_description_dialog import SetDescriptionDialog from .widgets.settings_widget import SettingsWidget @@ -266,6 +268,8 @@ def connect_signals(self): self.ui.actionStart_jupyter_console.setMenu(self.kernels_menu) self.kernels_menu.aboutToShow.connect(self.fetch_kernels) self.kernels_menu.aboutToHide.connect(self.stop_fetching_kernels) + self.ui.actionStart_default_python_in_basic_console.triggered.connect(self.start_detached_python_basic_console) + self.ui.actionStart_default_julia_in_basic_console.triggered.connect(self.start_detached_julia_basic_console) self.ui.actionSave.triggered.connect(self.save_project) self.ui.actionSave_As.triggered.connect(self.save_project_as) self.ui.actionClose.triggered.connect(lambda _checked=False: self.close_project()) @@ -2135,7 +2139,7 @@ def _add_item_edit_actions(self): self.ui.actionRemove, ] for action in actions: - action.setShortcutContext(Qt.WidgetShortcut) + action.setShortcutContext(Qt.ShortcutContext.WidgetShortcut) self.ui.graphicsView.addAction(action) @Slot(str, str) @@ -2148,7 +2152,7 @@ def _show_error_box(self, title, message): """Shows an error message with the given title and message.""" box = QErrorMessage(self) box.setWindowTitle(title) - box.setWindowModality(Qt.ApplicationModal) + box.setWindowModality(Qt.WindowModality.ApplicationModal) box.showMessage(message) def _connect_project_signals(self): @@ -2295,7 +2299,8 @@ def _rename_project_item(self, _): """Renames active project item.""" item = self.active_project_item answer = QInputDialog.getText( - self, "Rename Item", "New name:", text=item.name, flags=Qt.WindowTitleHint | Qt.WindowCloseButtonHint + self, "Rename Item", "New name:", text=item.name, + flags=Qt.WindowType.WindowTitleHint | Qt.WindowType.WindowCloseButtonHint ) if not answer[1]: return @@ -2331,6 +2336,54 @@ def project_item_context_menu(self, additional_actions): menu.aboutToHide.connect(self.enable_edit_actions) return menu + @Slot() + def start_detached_python_basic_console(self): + """Starts basic console with the default Python interpreter.""" + python = resolve_python_interpreter(self.qsettings()) + _set_resource_limits(self.qsettings(), threading.Lock()) + self.start_detached_basic_console("python", python) + + @Slot() + def start_detached_julia_basic_console(self): + """Starts basic console with the default Julia executable.""" + julia = resolve_julia_executable(self.qsettings()) + project = resolve_julia_project(self.qsettings()) + if not julia: + self.msg_warning.emit("No Julia installation found. Add path to a Julia executable in Spine " + "Toolbox Settings [File->Settings->Tools]") + return + _set_resource_limits(self.qsettings(), threading.Lock()) + self.start_detached_basic_console("julia", julia, project) + + def start_detached_basic_console(self, language, executable, julia_project=None): + """Launches a new detached basic console with the given executable + or activates an existing Console if the kernel is already running. + + Args: + language (str): Console kernel language + executable (str): Abs. path to kernel file + julia_project (str): Path to Julia environment + """ + for pcw in self._persistent_consoles.values(): + if pcw.detached_console_id is not None: + if os.path.samefile(pcw.detached_console_id, executable) and None in pcw.owners: + # Console running the requested kernel already exists, show and activate it + if pcw.parent().isMinimized(): + pcw.parent().showNormal() + pcw.parent().activateWindow() + return + icon = basic_console_icon(language) + console_window = ConsoleWindow(icon, language) + c = PersistentConsoleWidget(self, None, language, None, console_window) + key = c.request_start_kernel(executable, julia_project) + if not key: + self.msg_error.emit(f"Starting Basic Console for {executable} failed") + return + c.insert_banner(language) + self._persistent_consoles[key] = c + console_window.console_closed.connect(self._cleanup_basic_console) + c.show() + @Slot(str, QIcon, bool) def start_detached_jupyter_console(self, kernel_name, icon, conda): """Launches a new detached Console with the given kernel @@ -2478,6 +2531,17 @@ def _cleanup_jupyter_console(self, conn_file): engine_mngr = make_engine_manager(exec_remotely) engine_mngr.shutdown_kernel(conn_file) + @Slot(str) + def _cleanup_basic_console(self, key): + """Removes reference to the Basic Console and closes the kernel manager on Engine.""" + c = self._persistent_consoles.pop(key, None) + if not c: + return + c.shutdown_executor() + exec_remotely = self.qsettings().value("engineSettings/remoteExecutionEnabled", "false") == "true" + engine_mngr = make_engine_manager(exec_remotely) + engine_mngr.kill_persistent(key) + def _shutdown_engine_kernels(self): """Shuts down all persistent and Jupyter kernels managed by Spine Engine.""" exec_remotely = self.qsettings().value("engineSettings/remoteExecutionEnabled", "false") == "true" diff --git a/spinetoolbox/widgets/persistent_console_widget.py b/spinetoolbox/widgets/persistent_console_widget.py index 3c5b35827..482887da7 100644 --- a/spinetoolbox/widgets/persistent_console_widget.py +++ b/spinetoolbox/widgets/persistent_console_widget.py @@ -12,7 +12,10 @@ """Contains a widget acting as a console for Julia & Python REPL's.""" import os +import sys import uuid +import multiprocessing +from queue import Empty from pygments.lexers import get_lexer_by_name from pygments.styles import get_style_by_name from pygments.token import Token @@ -28,8 +31,13 @@ QTextCursor, QTextOption, ) -from PySide6.QtWidgets import QPlainTextEdit, QSizePolicy +from PySide6.QtWidgets import QPlainTextEdit, QSizePolicy, QWidget, QVBoxLayout from spine_engine.exception import RemoteEngineInitFailed +from spine_engine.execution_managers.persistent_execution_manager import ( + PythonPersistentExecutionManager, + JuliaPersistentExecutionManager, +) +from spine_engine.utils.queue_logger import QueueLogger from spinetoolbox.helpers import CustomSyntaxHighlighter from spinetoolbox.qthread_pool_executor import QtBasedThreadPoolExecutor from spinetoolbox.spine_engine_manager import make_engine_manager @@ -42,13 +50,13 @@ def __init__(self, console): self._console = console self._current_prompt = "" self.setStyleSheet("QPlainTextEdit {background-color: transparent; color: transparent; border:none;}") - self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) - self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) + self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) self.document().setDocumentMargin(0) - self.setAttribute(Qt.WA_TransparentForMouseEvents) + self.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents) self.setTabChangesFocus(False) self.setUndoRedoEnabled(False) - self.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding) + self.setSizePolicy(QSizePolicy.Policy.MinimumExpanding, QSizePolicy.Policy.MinimumExpanding) self.cursorPositionChanged.connect(self._handle_cursor_position_changed) self.textChanged.connect(self._handle_text_changed) @@ -116,10 +124,10 @@ def _handle_cursor_position_changed(self): self._updating = False def keyPressEvent(self, ev): - if ev.matches(QKeySequence.Copy): + if ev.matches(QKeySequence.StandardKey.Copy): ev.ignore() return - if ev.key() == Qt.Key_Backspace: + if ev.key() == Qt.Key.Key_Backspace: cursor = self.textCursor() if cursor.position() == self.min_pos: return @@ -131,13 +139,13 @@ def keyPressEvent(self, ev): ) cursor.removeSelectedText() return - if ev.key() == Qt.Key_Left: + if ev.key() == Qt.Key.Key_Left: cursor = self.textCursor() if cursor.positionInBlock() == self.new_line_indent: cursor.movePosition(QTextCursor.MoveOperation.PreviousCharacter, n=self.new_line_indent + 1) self.setTextCursor(cursor) return - if ev.key() == Qt.Key_Delete: + if ev.key() == Qt.Key.Key_Delete: cursor = self.textCursor() if cursor.atBlockEnd(): cursor.movePosition( @@ -166,18 +174,22 @@ class PersistentConsoleWidget(QPlainTextEdit): _MAX_LINES_PER_CYCLE = _MAX_LINES_PER_SECOND * 1000 / _FLUSH_INTERVAL _MAX_LINES_COUNT = 2000 - def __init__(self, toolbox, key, language, owner=None): + def __init__(self, toolbox, key, language, owner=None, console=None): """ Args: toolbox (ToolboxUI) key (tuple): persistent process identifier language (str): for syntax highlighting and prompting, etc. owner (ProjectItemBase, optional): console owner + console (ConsoleWindow): A window for displaying the console if running in detached mode """ - super().__init__(parent=toolbox) + parent_widget = toolbox if not console else console + super().__init__(parent=parent_widget) + if console is not None: + console.lay_out.addWidget(self) self._executor = QtBasedThreadPoolExecutor(max_workers=1) self._updating = False - font = QFontDatabase.systemFont(QFontDatabase.FixedFont) + font = QFontDatabase.systemFont(QFontDatabase.SystemFont.FixedFont) self.setFont(font) self.setMaximumBlockCount(self._MAX_LINES_COUNT) self._toolbox = toolbox @@ -198,12 +210,12 @@ def __init__(self, toolbox, key, language, owner=None): f"QPlainTextEdit {{background-color: {background_color}; color: {foreground_color}; border: 0px}}" ) cursor_width = self.fontMetrics().horizontalAdvance("x") - self.setWordWrapMode(QTextOption.WrapAnywhere) + self.setWordWrapMode(QTextOption.WrapMode.WrapAnywhere) self.setTabStopDistance(4 * cursor_width) self._line_edit = _CustomLineEdit(self) self._line_edit.setFont(font) self._line_edit.setCursorWidth(cursor_width) - self._line_edit.setWordWrapMode(QTextOption.WrapAnywhere) + self._line_edit.setWordWrapMode(QTextOption.WrapMode.WrapAnywhere) self._line_edit.setTabStopDistance(4 * cursor_width) self._highlighter = CustomSyntaxHighlighter(None) self._highlighter.set_style(self._style) @@ -236,9 +248,19 @@ def __init__(self, toolbox, key, language, owner=None): self._completions_available.connect(self._display_completions) self._restarted.connect(self._handle_restarted) self._killed.connect(self._do_set_killed) + self._execution_manager = None + self._q = multiprocessing.Queue() + self._logger = QueueLogger(self._q, "DetachedBasicConsole", None, {}) + self._console = console + self.detached_console_id = None def closeEvent(self, ev): super().closeEvent(ev) + self.shutdown_executor() + if isinstance(self.parent(), ConsoleWindow): + self.parent().close() + + def shutdown_executor(self): self._executor.shutdown() def name(self): @@ -263,9 +285,9 @@ def focusInEvent(self, ev): def mouseMoveEvent(self, ev): super().mouseMoveEvent(ev) if self.anchorAt(ev.position().toPoint()): - self.viewport().setCursor(Qt.PointingHandCursor) + self.viewport().setCursor(Qt.CursorShape.PointingHandCursor) else: - self.viewport().setCursor(Qt.IBeamCursor) + self.viewport().setCursor(Qt.CursorShape.IBeamCursor) def mousePressEvent(self, ev): super().mousePressEvent(ev) @@ -279,7 +301,7 @@ def mouseReleaseEvent(self, ev): if text_buffer is None: return cursor = self.cursorForPosition(ev.position().toPoint()) - cursor.select(cursor.BlockUnderCursor) + cursor.select(cursor.SelectionType.BlockUnderCursor) cursor.removeSelectedText() cursor.beginEditBlock() while text_buffer: @@ -398,9 +420,9 @@ def _flush_text_buffer(self): def _make_prompt(self): text_format = QTextCharFormat() if self._language == "julia": - prompt = "\njulia> " - text_format.setForeground(Qt.darkGreen) - text_format.setFontWeight(QFont.Bold) + prompt = "julia> " + text_format.setForeground(Qt.GlobalColor.darkGreen) + text_format.setFontWeight(QFont.Weight.Bold) elif self._language == "python": prompt = ">>> " else: @@ -548,13 +570,13 @@ def key_press_event(self, ev): """ self._at_bottom = True text = self._get_current_text() - if ev.key() in (Qt.Key_Return, Qt.Key_Enter): + if ev.key() in (Qt.Key.Key_Return, Qt.Key.Key_Enter): self._issue_command(text) - elif ev.key() == Qt.Key_Up: + elif ev.key() == Qt.Key.Key_Up: self._move_history(text, True) - elif ev.key() == Qt.Key_Down: + elif ev.key() == Qt.Key.Key_Down: self._move_history(text, False) - elif ev.key() == Qt.Key_Tab: + elif ev.key() == Qt.Key.Key_Tab: self._autocomplete(text) else: return False @@ -585,12 +607,12 @@ def _issue_command(self, text): Args: text (str) """ + if not text.strip(): # Don't send empty command to executor + self._make_prompt_block(prompt=self._prompt) + return self._executor.submit(self._do_check_command, text) def _do_check_command(self, text): - if not text.strip(): # Don't send empty command to execution manager - self._make_prompt_block(prompt=self._prompt) - return engine_mngr = self.create_engine_manager() if not engine_mngr: return @@ -769,6 +791,95 @@ def contextMenuEvent(self, ev): self._extend_menu(menu) menu.exec(ev.globalPos()) + def request_start_kernel(self, exec_path, julia_project=None): + """Requests Spine Engine to launch a persistent kernel manager for the given Python. + + Args: + exec_path (str): Abs. path to kernel file (e.g. ../../julia.exe or ../../python.exe) + julia_project (str): Path to Julia environment + + Returns: + str or None: Kernel manager key if kernel manager was launched successfully, None otherwise + """ + args = [exec_path] + if self._language == "python": + manager_class = PythonPersistentExecutionManager + elif self._language == "julia": + manager_class = JuliaPersistentExecutionManager + if julia_project: + args += ["--project=" + julia_project] + else: + self._logger.msg_error.emit(f"Unsupported console language '{self._language}'") + return None + self._execution_manager = manager_class( + self._logger, args, [], f"Detached Basic {self._language.capitalize()} Console", False, None + ) + try: + msg_type, msg = self._q.get(timeout=20) # Blocks until msg (tuple(str, dict) is received, or timeout. + except Empty: + msg_type, msg = "No response from Engine", {} + if msg_type != "persistent_execution_msg": + self._toolbox.msg_error.emit(f"Starting console failed: {msg_type} [{msg}]") + self.release_exec_mngr_resources() + return None + else: + retval = self._handle_exec_mngr_started_msg(msg) + self.detached_console_id = exec_path + return retval + + def insert_banner(self, language): + """Inserts banner for the detached Python console on Windows. + + Note: Julia consoles on Windows are missing the banner. You can get the banner + in Julia 1.11 with REPL.banner() but this function doesn't exist on older Julias + (e.g. Julia 1.8). On older Julias, you can get the banner using the Base.banner() + function, which doesn't exist on Julia 1.11. + """ + if sys.platform != "win32": + return + if language == "python": + engine_mngr = self.create_engine_manager() + if not engine_mngr: + return + sys_version = "" + sys_platform = "" + for msg in engine_mngr.issue_persistent_command(self._key, "sys.version"): + if msg["type"] == "stdout": + sys_version = msg["data"] + for msg in engine_mngr.issue_persistent_command(self._key, "sys.platform"): + if msg["type"] == "stdout": + sys_platform = msg["data"] + banner = "Python " + sys_version.replace("'", "") + " on " + sys_platform.replace("'", "") + self._insert_text_before_prompt(banner) + + def release_exec_mngr_resources(self): + """Closes _io.TextIOWrapper files.""" + if self._execution_manager is not None: + self._execution_manager.std_out.close() + self._execution_manager.std_err.close() + self._execution_manager = None + + def _handle_exec_mngr_started_msg(self, msg): + """Handles the response message from PythonPersistentExecutionManager. + + Args: + msg (dict): Message with item_name, type, etc. keys + + Returns: + str or None: Persistent process key if engine started the requested kernel + manager successfully, None otherwise. + """ + if msg["type"] == "persistent_started": + self._key = msg["key"] + self.parent().set_key(self._key) + return self._key + if msg["type"] == "persistent_failed_to_start": + self._toolbox.msg_error.emit(f"Basic Console failed to start:
{msg}") + return None + else: + self._toolbox.msg.emit(f"Unhandled message: {msg}") + return None + # Translated from # https://code.qt.io/cgit/qt-creator/qt-creator.git/tree/src/libs/utils/ansiescapecodehandler.cpp?h=master @@ -787,10 +898,10 @@ def _make_default_format(self): default_format.setForeground(self._fg_color) return default_format - def endFormatScope(self): + def end_format_scope(self): self._previous_format_closed = True - def setFormatScope(self, char_format): + def set_format_scope(self, char_format): self._previous_format = char_format self._previous_format_closed = False @@ -880,55 +991,55 @@ class AnsiEscapeCode: stripped_text = stripped_text[1:] if not numbers: char_format = self._make_default_format() - self.endFormatScope() + self.end_format_scope() for i in range(len(numbers)): # pylint: disable=consider-using-enumerate code = int(numbers[i]) if AnsiEscapeCode.TextColorStart <= code <= AnsiEscapeCode.TextColorEnd: char_format.setForeground(_ansi_color(code - AnsiEscapeCode.TextColorStart)) - self.setFormatScope(char_format) + self.set_format_scope(char_format) elif AnsiEscapeCode.BrightTextColorStart <= code <= AnsiEscapeCode.BrightTextColorEnd: char_format.setForeground(_ansi_color(code - AnsiEscapeCode.BrightTextColorStart, bright=True)) - self.setFormatScope(char_format) + self.set_format_scope(char_format) elif AnsiEscapeCode.BackgroundColorStart <= code <= AnsiEscapeCode.BackgroundColorEnd: char_format.setBackground(_ansi_color(code - AnsiEscapeCode.BackgroundColorStart)) - self.setFormatScope(char_format) + self.set_format_scope(char_format) elif AnsiEscapeCode.BrightBackgroundColorStart <= code <= AnsiEscapeCode.BrightBackgroundColorEnd: char_format.setBackground( _ansi_color(code - AnsiEscapeCode.BrightBackgroundColorStart, bright=True) ) - self.setFormatScope(char_format) + self.set_format_scope(char_format) else: if code == AnsiEscapeCode.ResetFormat: char_format = self._make_default_format() - self.endFormatScope() + self.end_format_scope() break if code == AnsiEscapeCode.BoldText: - char_format.setFontWeight(QFont.Bold) - self.setFormatScope(char_format) + char_format.setFontWeight(QFont.Weight.Bold) + self.set_format_scope(char_format) break if code == AnsiEscapeCode.FaintText: - char_format.setFontWeight(QFont.Light) - self.setFormatScope(char_format) + char_format.setFontWeight(QFont.Weight.Light) + self.set_format_scope(char_format) break if code == AnsiEscapeCode.ItalicText: char_format.setFontItalic(True) - self.setFormatScope(char_format) + self.set_format_scope(char_format) break if code == AnsiEscapeCode.NormalIntensity: - char_format.setFontWeight(QFont.Normal) - self.setFormatScope(char_format) + char_format.setFontWeight(QFont.Weight.Normal) + self.set_format_scope(char_format) break if code == AnsiEscapeCode.NotItalic: char_format.setFontItalic(False) - self.setFormatScope(char_format) + self.set_format_scope(char_format) break if code == AnsiEscapeCode.DefaultTextColor: char_format.setForeground(self._fg_color) - self.setFormatScope(char_format) + self.set_format_scope(char_format) break if code == AnsiEscapeCode.DefaultBackgroundColor: char_format.setBackground(self._bg_color) - self.setFormatScope(char_format) + self.set_format_scope(char_format) break if code == AnsiEscapeCode.RgbBackgroundColor: # See http://en.wikipedia.org/wiki/ANSI_escape_code#Colors @@ -944,7 +1055,7 @@ class AnsiEscapeCode: char_format.setForeground(color) else: char_format.setBackground(color) - self.setFormatScope(char_format) + self.set_format_scope(char_format) i += 3 break if j == 5: @@ -969,7 +1080,7 @@ class AnsiEscapeCode: char_format.setForeground(color) else: char_format.setBackground(color) - self.setFormatScope(char_format) + self.set_format_scope(char_format) i += 1 break break @@ -984,3 +1095,25 @@ def _ansi_color(code, bright=False): green = on if code & 2 else off blue = on if code & 4 else off return QColor(red, green, blue) + + +class ConsoleWindow(QWidget): + """A window for displaying a detached basic console.""" + + console_closed = Signal(str) + + def __init__(self, icon, language): + super().__init__(parent=None, f=Qt.WindowType.Window) + self.setWindowIcon(icon) + self.resize(600, 350) + self.lay_out = QVBoxLayout(self) + self.lay_out.setContentsMargins(0, 0, 0, 0) + self._key = None + self.setWindowTitle(f"{language.capitalize()}" + " Basic Console [Detached]") + self.show() + + def set_key(self, key): + self._key = key + + def closeEvent(self, event): + self.console_closed.emit(self._key) diff --git a/tests/mock_helpers.py b/tests/mock_helpers.py index 78370e99a..238e1e638 100644 --- a/tests/mock_helpers.py +++ b/tests/mock_helpers.py @@ -67,7 +67,6 @@ def create_toolboxui_with_project(project_dir): mock.patch("spinetoolbox.ui_main.QSettings.setValue"), mock.patch("spinetoolbox.ui_main.QSettings.sync"), mock.patch("spinetoolbox.plugin_manager.PluginManager.load_installed_plugins"), - mock.patch("spinetoolbox.ui_main.QScrollArea.setWidget"), ): mock_qsettings_value.side_effect = qsettings_value_side_effect mock_set_app_style.return_value = True diff --git a/tests/test_ToolboxUI.py b/tests/test_ToolboxUI.py index b7a965640..78064e68a 100644 --- a/tests/test_ToolboxUI.py +++ b/tests/test_ToolboxUI.py @@ -11,6 +11,7 @@ ###################################################################################################################### """Unit tests for ToolboxUI class.""" +import sys from contextlib import contextmanager import json import os @@ -28,7 +29,7 @@ from spinetoolbox.project_item.project_item import ProjectItem from spinetoolbox.resources_icons_rc import qInitResources import spinetoolbox.ui_main -from spinetoolbox.widgets.persistent_console_widget import PersistentConsoleWidget +from spinetoolbox.widgets.persistent_console_widget import PersistentConsoleWidget, ConsoleWindow from spinetoolbox.widgets.project_item_drag import NiceButton, ProjectItemDragMixin from .mock_helpers import ( TestCaseWithQApplication, @@ -219,7 +220,7 @@ def test_prevent_project_closing_with_unsaved_changes(self): # Make sure that the test uses LocalSpineEngineManager mock_qsettings_value.side_effect = qsettings_value_side_effect # Selecting cancel on the project close confirmation - with mock.patch.object(QMessageBox, "exec", return_value=QMessageBox.Cancel): + with mock.patch.object(QMessageBox, "exec", return_value=QMessageBox.StandardButton.Cancel): self.assertFalse(self.toolbox.close_project()) mock_qsettings_value.assert_called() with ( @@ -227,7 +228,7 @@ def test_prevent_project_closing_with_unsaved_changes(self): mock.patch("spinetoolbox.project.create_dir"), mock.patch("spinetoolbox.project_item.project_item.create_dir"), mock.patch("spinetoolbox.ui_main.ToolboxUI.update_recent_projects"), - mock.patch.object(QMessageBox, "exec", return_value=QMessageBox.Cancel), + mock.patch.object(QMessageBox, "exec", return_value=QMessageBox.StandardButton.Cancel), ): # Selecting cancel on the project close confirmation with mock.patch("spinetoolbox.ui_main.ToolboxUI.add_warning_message") as warning_msg: @@ -336,7 +337,7 @@ def test_selection_in_design_view_1(self): dc1_item = self.toolbox.project().get_item(dc1) dc1_center_point = self.find_click_point_of_pi(dc1_item, gv) # Center point in graphics view viewport coords. # Simulate mouse click on Data Connection in Design View - QTest.mouseClick(gv.viewport(), Qt.LeftButton, Qt.NoModifier, dc1_center_point) + QTest.mouseClick(gv.viewport(), Qt.MouseButton.LeftButton, Qt.KeyboardModifier.NoModifier, dc1_center_point) self.assertEqual(1, len(gv.scene().selectedItems())) # Active project item should be DC1 self.assertEqual(self.toolbox.project().get_item(dc1), self.toolbox.active_project_item) @@ -359,9 +360,9 @@ def test_selection_in_design_view_2(self): dc1_center_point = self.find_click_point_of_pi(dc1_item, gv) dc2_center_point = self.find_click_point_of_pi(dc2_item, gv) # Mouse click on dc1 - QTest.mouseClick(gv.viewport(), Qt.LeftButton, Qt.NoModifier, dc1_center_point) + QTest.mouseClick(gv.viewport(), Qt.MouseButton.LeftButton, Qt.KeyboardModifier.NoModifier, dc1_center_point) # Then mouse click on dc2 - QTest.mouseClick(gv.viewport(), Qt.LeftButton, Qt.NoModifier, dc2_center_point) + QTest.mouseClick(gv.viewport(), Qt.MouseButton.LeftButton, Qt.KeyboardModifier.NoModifier, dc2_center_point) self.assertEqual(1, len(gv.scene().selectedItems())) # Active project item should be DC2 self.assertEqual(self.toolbox.project().get_item(dc2), self.toolbox.active_project_item) @@ -378,9 +379,9 @@ def test_selection_in_design_view_3(self): dc1_item = self.toolbox.project().get_item(dc1) dc1_center_point = self.find_click_point_of_pi(dc1_item, gv) # Mouse click on dc1 - QTest.mouseClick(gv.viewport(), Qt.LeftButton, Qt.NoModifier, dc1_center_point) + QTest.mouseClick(gv.viewport(), Qt.MouseButton.LeftButton, Qt.KeyboardModifier.NoModifier, dc1_center_point) # Then mouse click somewhere else in Design View (not on project item) - QTest.mouseClick(gv.viewport(), Qt.LeftButton, Qt.NoModifier, QPoint(1, 1)) + QTest.mouseClick(gv.viewport(), Qt.MouseButton.LeftButton, Qt.KeyboardModifier.NoModifier, QPoint(1, 1)) self.assertEqual(0, len(gv.scene().selectedItems())) # No items in design view should be selected # Active project item should be None self.assertIsNone(self.toolbox.active_project_item) @@ -410,7 +411,7 @@ def test_selection_in_design_view_4(self): self.assertEqual(1, len(links)) link_center_point = self.find_click_point_of_link(links[0], gv) # Mouse click on link - QTest.mouseClick(gv.viewport(), Qt.LeftButton, Qt.NoModifier, link_center_point) + QTest.mouseClick(gv.viewport(), Qt.MouseButton.LeftButton, Qt.KeyboardModifier.NoModifier, link_center_point) # One item should be selected in Design View (the Link) selected_items = gv.scene().selectedItems() self.assertEqual(1, len(selected_items)) @@ -445,9 +446,9 @@ def test_selection_in_design_view_5(self): dc1_center_point = self.find_click_point_of_pi(dc1_item, gv) link_center_point = self.find_click_point_of_link(links[0], gv) # Mouse click on dc1 - QTest.mouseClick(gv.viewport(), Qt.LeftButton, Qt.NoModifier, dc1_center_point) + QTest.mouseClick(gv.viewport(), Qt.MouseButton.LeftButton, Qt.KeyboardModifier.NoModifier, dc1_center_point) # Mouse click on link - QTest.mouseClick(gv.viewport(), Qt.LeftButton, Qt.NoModifier, link_center_point) + QTest.mouseClick(gv.viewport(), Qt.MouseButton.LeftButton, Qt.KeyboardModifier.NoModifier, link_center_point) # One item should be selected in Design View (the Link) selected_items = gv.scene().selectedItems() self.assertEqual(1, len(selected_items)) @@ -475,9 +476,13 @@ def test_selection_in_design_view_6(self): dc1_center_point = self.find_click_point_of_pi(dc1_item, gv) dc2_center_point = self.find_click_point_of_pi(dc2_item, gv) # Mouse click on dc1 - QTest.mouseClick(gv.viewport(), Qt.LeftButton, Qt.ControlModifier, dc1_center_point) + QTest.mouseClick( + gv.viewport(), Qt.MouseButton.LeftButton, Qt.KeyboardModifier.ControlModifier, dc1_center_point + ) # Then mouse click on dc2 - QTest.mouseClick(gv.viewport(), Qt.LeftButton, Qt.ControlModifier, dc2_center_point) + QTest.mouseClick( + gv.viewport(), Qt.MouseButton.LeftButton, Qt.KeyboardModifier.ControlModifier, dc2_center_point + ) self.assertEqual(2, len(gv.scene().selectedItems())) # Active project item should be None self.assertIsNone(self.toolbox.active_project_item) @@ -486,7 +491,9 @@ def test_drop_invalid_drag_on_design_view(self): mime_data = QMimeData() gv = self.toolbox.ui.graphicsView pos = QPoint(0, 0) - event = QDropEvent(pos, Qt.CopyAction, mime_data, Qt.NoButton, Qt.NoModifier) + event = QDropEvent( + pos, Qt.DropAction.CopyAction, mime_data, Qt.MouseButton.NoButton, Qt.KeyboardModifier.NoModifier + ) with ( mock.patch("PySide6.QtWidgets.QGraphicsSceneDragDropEvent.source") as mock_drop_event_source, mock.patch.object(self.toolbox, "project"), @@ -505,7 +512,9 @@ def test_drop_project_item_on_design_view(self): gv = self.toolbox.ui.graphicsView scene_pos = QPointF(44, 20) pos = gv.mapFromScene(scene_pos) - event = QDropEvent(pos, Qt.CopyAction, mime_data, Qt.NoButton, Qt.NoModifier) + event = QDropEvent( + pos, Qt.DropAction.CopyAction, mime_data, Qt.MouseButton.NoButton, Qt.KeyboardModifier.NoModifier + ) with ( mock.patch("PySide6.QtWidgets.QGraphicsSceneDragDropEvent.source") as mock_drop_event_source, mock.patch.object(self.toolbox, "project"), @@ -725,11 +734,58 @@ def test_filtered_persistent_consoles_requested(self): ind = view.model().index(row, 0) view.scrollTo(ind) rect = view.visualRect(ind) - QTest.mouseClick(view.viewport(), Qt.LeftButton, Qt.ControlModifier, rect.center()) + QTest.mouseClick( + view.viewport(), Qt.MouseButton.LeftButton, Qt.KeyboardModifier.ControlModifier, rect.center() + ) console = self.toolbox.ui.splitter_console.widget(1) self.assertTrue(isinstance(console, PersistentConsoleWidget)) self.assertEqual(console.owners, {item}) + def test_detached_python_basic_console(self): + with ( + mock.patch("spinetoolbox.widgets.persistent_console_widget.ConsoleWindow.show") as mock_show, + mock.patch("spinetoolbox.ui_main.resolve_python_interpreter") as mock_resolve_python, + ): + mock_resolve_python.return_value = sys.executable + self.toolbox.ui.actionStart_default_python_in_basic_console.trigger() + mock_resolve_python.assert_called() + mock_show.assert_called() + self.assertEqual(len(self.toolbox._persistent_consoles), 1) + pcw = self.toolbox._persistent_consoles[list(self.toolbox._persistent_consoles.keys())[0]] + self.assertIsInstance(pcw, PersistentConsoleWidget) + self.assertIsInstance(pcw.parent(), ConsoleWindow) + self.assertTrue(pcw.detached_console_id, sys.executable) + pcw.parent().close() # Send close event to ConsoleWindow + self.assertEqual(len(self.toolbox._persistent_consoles), 0) + + def test_detached_julia_basic_console(self): + with ( + mock.patch("spinetoolbox.widgets.persistent_console_widget.ConsoleWindow.show") as mock_show, + mock.patch("spinetoolbox.ui_main.resolve_julia_executable") as mock_resolve_julia, + mock.patch("spinetoolbox.ui_main.resolve_julia_project") as mock_resolve_julia_project, + mock.patch( + "spinetoolbox.widgets.persistent_console_widget.JuliaPersistentExecutionManager" + ) as mock_julia_manager_class, + mock.patch("spinetoolbox.widgets.persistent_console_widget.multiprocessing.Queue") as mock_queue, + ): + mock_resolve_julia.return_value = "/some/julia" + mock_resolve_julia_project.return_value = "/some/julia/env" + mock_queue.return_value = MockQueue() + self.toolbox.ui.actionStart_default_julia_in_basic_console.trigger() + mock_show.assert_called() + mock_resolve_julia.assert_called() + mock_resolve_julia_project.assert_called() + mock_julia_manager_class.assert_called_once() + self.assertEqual(mock_julia_manager_class.call_args.args[1], ["/some/julia", "--project=/some/julia/env"]) + mock_queue.assert_called() + self.assertEqual(len(self.toolbox._persistent_consoles), 1) + pcw = self.toolbox._persistent_consoles[list(self.toolbox._persistent_consoles.keys())[0]] + self.assertIsInstance(pcw, PersistentConsoleWidget) + self.assertIsInstance(pcw.parent(), ConsoleWindow) + self.assertTrue(pcw.detached_console_id, "/some/julia") + pcw.parent().close() + self.assertEqual(len(self.toolbox._persistent_consoles), 0) + def test_closeEvent_saves_window_state(self): self.toolbox._qsettings = mock.NonCallableMagicMock() self.toolbox._perform_pre_exit_tasks = mock.MagicMock(return_value=True) @@ -890,5 +946,10 @@ def exec(self, pos): return True +class MockQueue: + def get(self, timeout=1): + return "persistent_execution_msg", {"type": "persistent_started", "key": "123"} + + if __name__ == "__main__": unittest.main()