diff --git a/constants.py b/constants.py index 9372a0b..8bf1281 100644 --- a/constants.py +++ b/constants.py @@ -34,7 +34,7 @@ "javascript": [".js", ".jsx", ".ts", ".tsx"], "json": [".json"], "lua": [".lua"], - "nim": [".nim", ".nims"], + "nim": [".nim", ".nims", ".nimble"], "oberon/modula": [".mod", ".ob", ".ob2", ".cp"], "octave": [".m"], "pascal": [".pas", ".pp", ".lpr", ".cyp"], diff --git a/gui/externalprogram.py b/gui/externalprogram.py new file mode 100644 index 0000000..1e5b196 --- /dev/null +++ b/gui/externalprogram.py @@ -0,0 +1,195 @@ +# -*- coding: utf-8 -*- + +""" +Copyright (c) 2013-2023 Matic Kukovec. +Released under the GNU GPL3 license. + +For more information check the 'LICENSE.txt' file. +For complete license information of the dependencies, check the 'additional_licenses' directory. +""" + +import os +import time +import subprocess +import win32api +import win32con +import win32gui +import win32process +import psutil + +import data +import functions +import components + +class ExternalWidget(data.QWidget): + handle_cache = [] + + name = None + _parent = None + main_form = None + current_icon = None + internals = None + savable = data.CanSave.NO + save_name = None + process_reference= None + window_reference = None + + def __init__(self, parent, main_form, name): + super().__init__(parent) + self.name = name + self._parent = parent + self.main_form = main_form + + self.current_icon = functions.create_icon("tango_icons/utilities-terminal.png") + self.internals = components.internals.Internals( + parent=self, tab_widget=parent + ) + self.internals.update_icon(self) + + self.update_style() + + self.my_hwnd = None + self.external_hwnd = None + + def __del__(self): + try: + self.main_form.removeEventFilter(self) + except: + pass + try: + ExternalWidget.handle_cache.remove(hwnd) + self.process_reference.kill() + except: + pass + + def set_my_hwnd(self, hwnd): + self.my_hwnd = hwnd + + def set_external_hwnd(self, hwnd): + self.external_hwnd = hwnd + ExternalWidget.handle_cache.append(hwnd) + + def set_process_reference(self, proc): + self.process_reference = proc + + def set_window_reference(self, window): + self.window_reference = window + + def update_style(self): + self.setStyleSheet(f""" +QWidget {{ + padding: 0px; + margin: 0px; + border: none; +}} + """) + + def eventFilter(self, object, event): + print("Object:", object, "Event-Type:", event.type()) + if event.type() in (data.QEvent.Type.Enter, data.QEvent.Type.MouseButtonPress, data.QEvent.Type.KeyPress): + print("ENTER") +# if self.my_hwnd: +# win32gui.SetFocus(self.my_hwnd) + elif event.type() == data.QEvent.Type.Leave: + print("LEAVE") +# if self.external_hwnd: +# win32gui.SetFocus(self.external_hwnd) + + return super().eventFilter(object, event) + +def __create_external_widget(hwnd, proc, name, parent, main_form): + main_widget = ExternalWidget(parent, main_form, name) + main_widget.name = name + main_widget.set_my_hwnd(int(main_widget.winId())) + main_widget.set_external_hwnd(hwnd) + main_widget.set_process_reference(proc) + + layout = data.QStackedLayout(main_widget) + layout.setStackingMode(data.QStackedLayout.StackingMode.StackAll) + layout.setSpacing(0) + layout.setContentsMargins(0, 0, 0, 0) + main_widget.setLayout(layout) + + window = data.QWindow.fromWinId(hwnd) + window.installEventFilter(main_form) + main_widget.installEventFilter(main_form) + main_widget.set_window_reference(window) + external_widget = data.QWidget.createWindowContainer( + window, + parent=main_widget, + flags=data.Qt.WindowType.FramelessWindowHint + ) + main_widget.layout().addWidget(external_widget) + def _initialize(*args): + external_widget.hide() + external_widget.show() + external_widget.update() + data.QTimer.singleShot(100, _initialize) + + return main_widget + + +def find_window_for_pid(pid): + result = None + def callback(hwnd, _): + nonlocal result + try: + if win32gui.IsWindowVisible(hwnd): + ctid, cpid = win32process.GetWindowThreadProcessId(hwnd) + pyhandle = win32api.OpenProcess(win32con.PROCESS_QUERY_INFORMATION | win32con.PROCESS_VM_READ, False, cpid) + proc_name = win32process.GetModuleFileNameEx(pyhandle, 0) + window_text = win32gui.GetWindowText(hwnd) +# print(pid, cpid, proc_name) + if int(cpid) == int(pid): + result = hwnd + return False + except: + pass + return True + try: + win32gui.EnumWindows(callback, None) + except: + pass + return result + +def create_external_widget(parent, main_form, program): +# program = "C:\\ProgramData\\chocolatey\\bin\\alacritty.exe" +# program = "C:\\ProgramData\\chocolatey\\bin\\nu.exe" +# program = "C:\\tools\\LibreSprite-Windows-x86_64\\libresprite.exe" +# program = "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe" +# program = "cmd.exe" +# program = "powershell.exe" + name = os.path.basename(program) + + # Old PID's, if any for the application + old_pids = [] + for proc in psutil.process_iter(['pid', 'name', 'username']): + if proc.info["name"] == name: + pid = int(proc.info["pid"]) + old_pids.append(pid) + + # Run the process + p = subprocess.Popen([program], creationflags=subprocess.CREATE_NEW_CONSOLE) + + # Embed the program + external_widget = None + found = False + for i in range(100): + for proc in psutil.process_iter(['pid', 'name', 'username']): + if proc.info["name"] == name: + pid = int(proc.info["pid"]) + if pid in old_pids: + continue + _hwnd = find_window_for_pid(pid) + if _hwnd is not None: + external_widget = __create_external_widget( + _hwnd, proc, name, parent, main_form + ) + found = True + break + else: + time.sleep(0.01) + if found: + break + + return external_widget \ No newline at end of file diff --git a/gui/mainwindow.py b/gui/mainwindow.py index 965f916..f581373 100644 --- a/gui/mainwindow.py +++ b/gui/mainwindow.py @@ -22,6 +22,7 @@ import traceback import gc import json + import data import components.actionfilter import components.communicator @@ -51,7 +52,10 @@ from .thebox import * from .hexview import * from .templates import * +from .externalprogram import * +if data.platform == "Windows": + import win32gui """ ------------------------------------------------- @@ -106,7 +110,8 @@ class MainWindow(data.QMainWindow): menubar_functions = {} # Last focused widget and tab needed by the function wheel overlay last_focused_widget = None - """Namespace references for grouping functionality""" + + # Namespace references for grouping functionality settings = None sessions = None view = None @@ -114,6 +119,9 @@ class MainWindow(data.QMainWindow): editing = None display = None bookmarks = None + + # External program reference + external_program = None def __init__(self, new_document=False, logging=False, file_arguments=None): @@ -227,6 +235,21 @@ def eventFilter(self, object, event): pass elif event.type()== data.QEvent.Type.WindowDeactivate: self.display.docking_overlay_hide() + +# print("Object:", object, "Event-Type:", event.type()) + if event.type() in (data.QEvent.Type.Enter, data.QEvent.Type.MouseButtonPress, data.QEvent.Type.KeyPress): +# print("ENTER") + if data.platform == "Windows": + win32gui.SetFocus(self.winId()) + elif event.type() == data.QEvent.Type.Leave: +# print("LEAVE") + if data.platform == "Windows": + def set_external_focus(): +# print(win32gui.GetWindowText(win32gui.GetForegroundWindow())) + handle = win32gui.WindowFromPoint(win32gui.GetCursorPos()) + if handle in ExternalWidget.handle_cache: + win32gui.SetFocus(handle) + data.QTimer.singleShot(50, set_external_focus) return False @@ -1898,6 +1921,38 @@ def open_general_explorer(): system_menu.addSeparator() system_menu.addAction(run_command_action) system_menu.addAction(show_terminal_action) + + # Terminals + if data.platform == "Windows": + # CMD + def add_cmd_terminal_emulator(): + terminal = self.get_helper_window().terminal_emulator_add( + "Terminal - CMD", "cmd.exe" + ) + self.get_helper_window().setCurrentWidget(terminal) + add_cmd_terminal_emulator_action = create_action( + "Add CMD Terminal", + None, + "Add a Windows CMD terminal emulator to the layout", + 'tango_icons/utilities-terminal.png', + add_cmd_terminal_emulator + ) + system_menu.addAction(add_cmd_terminal_emulator_action) + + # PowerShell + def add_powershell_terminal_emulator(): + terminal = self.get_helper_window().terminal_emulator_add( + "Terminal - PowerShell", "powershell.exe" + ) + self.get_helper_window().setCurrentWidget(terminal) + add_powershell_terminal_emulator_action = create_action( + "Add PowerShell Terminal", + None, + "Add a Windows PowerShell terminal emulator to the layout", + 'tango_icons/utilities-terminal.png', + add_powershell_terminal_emulator + ) + system_menu.addAction(add_powershell_terminal_emulator_action) #Lexers menu def construct_lexers_menu(parent): def set_lexer(lexer, lexer_name): @@ -5541,14 +5596,14 @@ def create_lexer(lexer, description): create_lexer(lexers.Matlab, 'Matlab'), lexers_menu ) -# NIM_action = create_action( -# 'Nim', -# None, -# 'Change document lexer to: Nim', -# 'language_icons/logo_nim.png', -# create_lexer(lexers.Nim, 'Nim'), -# lexers_menu -# ) + NIM_action = create_action( + 'Nim', + None, + 'Change document lexer to: Nim', + 'language_icons/logo_nim.png', + create_lexer(lexers.Nim, 'Nim'), + lexers_menu + ) OBERON_action = create_action( 'Oberon / Modula', None, @@ -5760,7 +5815,7 @@ def create_lexer(lexer, description): lexers_menu.addAction(LUA_action) lexers_menu.addAction(MAKEFILE_action) lexers_menu.addAction(MATLAB_action) -# lexers_menu.addAction(NIM_action) + lexers_menu.addAction(NIM_action) lexers_menu.addAction(OBERON_action) lexers_menu.addAction(Octave_action) lexers_menu.addAction(PASCAL_action) diff --git a/gui/tabwidget.py b/gui/tabwidget.py index 6d20f36..47ea499 100644 --- a/gui/tabwidget.py +++ b/gui/tabwidget.py @@ -26,6 +26,7 @@ from .dialogs import * from .menu import * from .treedisplays import * +from .externalprogram import * """ ----------------------------- @@ -607,6 +608,10 @@ def mousePressEvent(self, event): self.main_form.display.update_cursor_position() # Reset the click&drag context menu action components.actionfilter.ActionFilter.clear_action() + + widget = self.currentWidget() + if widget and isinstance(widget, ExternalWidget): + widget.window_reference.raise_() def wheelEvent(self, wheel_event): """ @@ -687,7 +692,9 @@ def __signal_editor_tabindex_change(self, change_event): data.signal_dispatcher.update_title.emit() def _signal_editor_tabclose(self, emmited_tab_number, force=False): - """Event that fires when a tab close""" + """ + Event that fires when a tab close + """ #Nested function for clearing all bookmarks in the document def clear_document_bookmarks(): #Check if bookmarks need to be cleared @@ -986,15 +993,24 @@ def tree_add_tab(self, tree_tab_name, tree_type=None): new_tree_tab_index = self.addTab(new_tree_tab, tree_tab_name) # Return the reference to the new added tree tab widget return self.widget(new_tree_tab_index) + + def terminal_emulator_add(self, tab_name, program): + new_external_tab = create_external_widget(self, self.main_form, program) + # Add the tree tab to the tab widget + new_tree_tab_index = self.addTab(new_external_tab, tab_name) def editor_update_margin(self): - """Update margin width according to the number of lines in the current document""" + """ + Update margin width according to the number of lines in the current document + """ #Check is the widget is a scintilla custom editor if isinstance(self.currentWidget(), CustomEditor): self.currentWidget().update_margin() def set_tab_name(self, tab, new_text): - """Set the name of a tab by passing a reference to it""" + """ + Set the name of a tab by passing a reference to it + """ #Cycle through all of the tabs for i in range(self.count()): if self.widget(i) == tab: