Skip to content

Commit

Permalink
added support for external programs (currently only on Windows)
Browse files Browse the repository at this point in the history
  • Loading branch information
matkuki committed May 26, 2023
1 parent 2b9b336 commit 9f2ff5e
Show file tree
Hide file tree
Showing 4 changed files with 280 additions and 14 deletions.
2 changes: 1 addition & 1 deletion constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
Expand Down
195 changes: 195 additions & 0 deletions gui/externalprogram.py
Original file line number Diff line number Diff line change
@@ -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
75 changes: 65 additions & 10 deletions gui/mainwindow.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import traceback
import gc
import json

import data
import components.actionfilter
import components.communicator
Expand Down Expand Up @@ -51,7 +52,10 @@
from .thebox import *
from .hexview import *
from .templates import *
from .externalprogram import *

if data.platform == "Windows":
import win32gui

"""
-------------------------------------------------
Expand Down Expand Up @@ -106,14 +110,18 @@ 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
system = None
editing = None
display = None
bookmarks = None

# External program reference
external_program = None


def __init__(self, new_document=False, logging=False, file_arguments=None):
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand Down
22 changes: 19 additions & 3 deletions gui/tabwidget.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
from .dialogs import *
from .menu import *
from .treedisplays import *
from .externalprogram import *

"""
-----------------------------
Expand Down Expand Up @@ -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):
"""
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down

0 comments on commit 9f2ff5e

Please sign in to comment.