# Setting up imports import sys import os from PyQt5.QtCore import Qt, QThread, pyqtSignal, QProcess from PyQt5.QtGui import QColor, QPalette, QFont, QIcon, QTextCursor from PyQt5.QtWidgets import ( QApplication, QMainWindow, QAction, QFileDialog, qApp, QLabel, QStatusBar, QPushButton, QProgressBar, ) from PyQt5.QtTest import QTest import platform import random from Hydra.widgets.Messagebox import MessageBox, NewProject from Hydra.utils.config import config_reader, config_choice, LOCATION from Hydra.widgets.Tabs import Tabs from Hydra.utils.last_open_file import update_previous_file, get_last_file from Hydra.widgets.Content import Content from Hydra.widgets.Image import Image from Hydra.widgets.Events import DeadCodeCheker from Hydra.utils.find_utility import DocumentSearch from Hydra.widgets.SaveFile import SaveFile from Hydra.widgets.Label import StatusLabel from Hydra.widgets.Browser import Browser from Hydra.resources.materialblack import material_blue from Hydra.utils.check_update import show_update, make_decision import shutil configs = [config_reader(0), config_reader(1), config_reader(2)] with open(LOCATION + "default.json") as choice: choiceIndex = int(choice.read()) # os.environ["PYTHONUNBUFFERED"] = "1" # This is just an environment variable that PyCharm uses class Main(QMainWindow): def __init__(self, app, palette, editor, parent=None): super().__init__(parent) self.editor = editor # Current config chosen (can be one of 3 config<N>.json) self.onStart(choiceIndex) # Initializing config options self.status = QStatusBar(self) # Status bar for displaying useful info like update found etc # Initializing the main widget where text is displayed self.tab = Tabs(self.cleanOpen, app, palette, self) # self.tabsOpen = [] self.pic_opened = False # This is used to open pictures but right now that feature is disabled self.dialog = MessageBox(self) # Handles dialogs, for now it only creates the create new project dialog self.setWindowIcon( QIcon("resources/Python-logo-notext.svg_.png") ) # Setting the window icon self.setWindowTitle("Hydra") # Setting the window title self.status_font = QFont(editor["statusBarFont"], editor["statusBarFontSize"]) # Status bar font self.os = platform.system() self.tab.tabs.currentChanged.connect(self.fileNameChange) # To change the title of the window when tab changes self.search = DocumentSearch() # To find documents in the whole system, also not quite working today # Initializing QActions that can be triggered from a QMenu or via keyboard shortcuts self.openterm() self.openterminal() # self.split2Tabs() self.new() self.newProject() self.findDocument() self.openProjectF() self.open() self.save() self.saveAs() self.exit() self.thread = UpdateThread() # Update checking runs on its own thread to prevent main GUI from blocking self.thread.start() # Data retrieved from the update thread gets processed check_updates self.thread.textSignal.connect(self.check_updates) # Attributes to manage opening directories and such self.dir_opened = False self._dir = None self.update_progress = QProgressBar() self.update_progress.setMaximumWidth(225) self.update_progress.setStyleSheet(self.update_progress.styleSheet()) self.setCentralWidget(self.tab) # QMainWindow's central widget self.files = None # Tracking the current file that is open self.dead_code_thread = DeadCodeCheker() # This checks for dead code self.dead_code_thread.infoSignal.connect(self.write_dead_code_info) self.stack = [] # Used for tracking when to check for dead code self.tab_to_write_to = None self.tagInfo = StatusLabel(text="", font=self.status_font) self.initUI() # Main UI def write_dead_code_info(self, text): self.tab.events.info_bar.setText(text) def check_updates(self, text): """ A function to check for updates and ask the user if they want to update or not """ self.update_label = QLabel() self.update_label.setFont( QFont(self.editor["generalFont"], self.editor["generalFontSize"]) ) self.update_label.setFont(self.status_font) self.update_label.setText(text) self.status.addWidget(self.update_label) if text != "An update is available, would you like to update?": pass else: self.button = QPushButton("Update") self.button.setFont( QFont(self.editor["generalFont"], self.editor["generalFontSize"]) ) self.status.addWidget(self.button) self.button.clicked.connect(self.update_Hydra) def update_Hydra(self): """ This function gets used when the user wants to update Hydra This function is not finished so it doesn't do any updating """ self.update_label.setText("Updating...") self.status.removeWidget(self.button) self.status.addWidget(self.update_progress) for i in range(101): self.update_progress.setValue(i) QTest.qWait(random.randint(50, 75)) # make_decision(True) def fileNameChange(self): try: currentFileName = self.tab.tabs.currentWidget().baseName self.setWindowTitle("Hydra ~ " + str(currentFileName)) except AttributeError: self.setWindowTitle("Hydra ~ ") def onStart(self, index): try: editor = configs[index]["editor"] if editor["windowStaysOnTop"] is True: self.setWindowFlags(Qt.WindowStaysOnTopHint) else: pass except Exception as err: pass # log exception self.font = QFont() self.font.setFamily(self.editor["editorFont"]) self.font.setPointSize(self.editor["editorFontSize"]) self.tabSize = self.editor["TabWidth"] def initUI(self): self.setStatusBar(self.status) # Initializing the status bar self.font.setFixedPitch(True) menuFont = QFont() menuFont.setFamily(self.editor["menuFont"]) menuFont.setPointSize(self.editor["menuFontSize"]) menu = self.menuBar() menu.setFont(menuFont) # Creating the file menu fileMenu = menu.addMenu("File") # Adding options to the file menu # self.setStatusBar(self.status) fileMenu.addAction(self.newAct) fileMenu.addAction(self.newProjectAct) fileMenu.addAction(self.openAct) fileMenu.addAction(self.openProjectAct) fileMenu.addAction(self.saveAct) fileMenu.addAction(self.saveAsAct) fileMenu.addSeparator() fileMenu.addAction(self.exitAct) toolMenu = menu.addMenu("Tools") toolMenu.addAction(self.openTermAct) toolMenu.addAction(self.openTerminalAct) # toolMenu.addAction(self.split2TabsAct) searchDoc = menu.addMenu("Find document") searchDoc.addAction(self.findDocumentAct) self.showMaximized() def open(self): self.openAct = QAction("Open...", self) self.openAct.setShortcut("Ctrl+O") self.openAct.setStatusTip("Open a file") self.openAct.triggered.connect(self.openFileFromMenu) def closeEvent(self, QCloseEvent): os._exit(42) # This makes sure every thread gets killed def new(self): self.newAct = QAction("New") self.newAct.setShortcut("Ctrl+N") self.newAct.setStatusTip("Create a new file") self.newAct.triggered.connect(self.newFile) def newProject(self): self.newProjectAct = QAction("New project") self.newProjectAct.setShortcut("Ctrl+Shift+N") self.newProjectAct.setStatusTip("Create a new project") self.newProjectAct.triggered.connect(self.newProjectFolder) def openProjectF(self): self.openProjectAct = QAction("Open project") self.openProjectAct.setShortcut("Ctrl+Shift+O") self.openProjectAct.setStatusTip("Open a project") self.openProjectAct.triggered.connect(self.openProject) def split2Tabs(self): self.split2TabsAct = QAction("Split the first 2 tabs") self.split2TabsAct.setShortcut("Ctrl+Alt+S") self.split2TabsAct.setStatusTip("Splits the first 2 tabs into one tab") self.split2TabsAct.triggered.connect(self.tab.split) def switchTabs(self): if self.tab.tabs.count() - 1 == self.tab.tabs.currentIndex(): self.tab.tabs.setCurrentIndex(0) else: self.tab.tabs.setCurrentIndex(self.tab.tabs.currentIndex() + 1) def save(self): self.saveAct = QAction("Save") self.saveAct.setShortcut("Ctrl+S") self.saveAct.setStatusTip("Save a file") self.saveAct.triggered.connect(self.saveFile) def openterm(self): self.openTermAct = QAction("Run", self) self.openTermAct.setShortcut("Shift+F10") self.openTermAct.setStatusTip("Run your code") self.openTermAct.triggered.connect(self.execute_file) def openterminal(self): self.openTerminalAct = QAction("Terminal", self) self.openTerminalAct.setShortcut("Ctrl+T") self.openTerminalAct.setStatusTip("Open a terminal") self.openTerminalAct.triggered.connect(self.realterminal) def saveAs(self): self.saveAsAct = QAction("Save As...") self.saveAsAct.setShortcut("Ctrl+Shift+S") self.saveAsAct.setStatusTip("Save a file as") self.saveAsAct.triggered.connect(self.saveFileAs) def findDocument(self): self.findDocumentAct = QAction("Find document") self.findDocumentAct.setShortcut("Ctrl+Shift+F") self.findDocumentAct.setStatusTip("Find a document") self.findDocumentAct.triggered.connect(self.temp) def temp(self): pass def findDocumentFunc(self): self.search.run() def exit(self): self.exitAct = QAction("Quit", self) self.exitAct.setShortcut("Ctrl+Q") self.exitAct.setStatusTip("Exit application") self.exitAct.triggered.connect(self.lets_exit) def lets_exit(self): # self.saveFile() qApp.quit() def openFileFromMenu(self): options = QFileDialog.Options() filenames, _ = QFileDialog.getOpenFileNames( self, "Open a file", "", "All Files (*);;Python Files (*.py);;Text Files (*.txt)", options=options, ) if filenames: # If file is selected, we can open it filename = filenames[0] if filename[-3:] in ["gif", "png", "jpg", "bmp"] or filename[-4:] in [ "jpeg" ]: self.pic_opened = True self.cleanOpen(filename, self.pic_opened) def openBrowser(self, url, word): widget = Browser(url) index = self.tab.tabs.addTab(widget, "Info about: " + str(word)) self.tab.tabs.setCurrentIndex(index) def cleanOpen(self, filename, pic_opened=False, searchCommand=None): basename = os.path.basename(filename) if os.path.isdir(filename): return if pic_opened: tab = Image(filename, basename) else: tab = Content("", filename, basename, self, False, searchCommand) for index, tab_name in enumerate(self.tab.tabCounter): if ( tab_name == basename ): # If we already have a file open and we're trying to open the same file, then do nothing if searchCommand: print(searchCommand, " search ocmmand") tab.searchFor(searchCommand) return tab.start_opening() # TODO: Only works for NON image files right now label = QLabel("Loading...") label.setAlignment(Qt.AlignCenter) index_to_remove = self.tab.tabs.addTab(label, "") # lmao it works tab.readyToShow.connect( lambda state: self.addTab(state, tab, basename, index_to_remove) ) update_previous_file(filename) def addTab(self, state, tab, basename, index_to_remove): """ Removes given tab and adds a new tab and makes it active """ self.tab.tabs.removeTab(index_to_remove) index = self.tab.tabs.addTab(tab, basename) self.tab.tabs.setCurrentIndex(index) self.tab.tabCounter.append(basename) # Not in use def openFile(self, filename): try: for index, tabName in enumerate(self.tab.tabCounter): with open(filename, "r+") as file_o: print("first open") if filename[-3:] in ["gif", "png", "jpg", "bmp"] or filename[ -4: ] in ["jpeg"]: self.pic_opened = True else: self.pic_opened = False try: text = file_o.read() except UnicodeDecodeError as E: text = str(E) basename = os.path.basename(filename) if not self.pic_opened: tab = Content(text, filename, basename, self) tab.saved = True tab.modified = False else: tab = Image(filename, basename) if tabName == tab.baseName: self.tab.tabs.removeTab(index) self.tab.tabCounter.remove(tab.baseName) try: with open(filename, "r+") as file_o: try: if self.pic_opened is not True: text = file_o.read() else: text = None except (FileNotFoundError, UnicodeDecodeError, AttributeError) as E: text = str(E) except FileNotFoundError: with open(filename, "w+") as newFileCreated: print("third open") text = newFileCreated.read() basename = os.path.basename(filename) if self.pic_opened is True: tab = Image(filename, basename) else: tab = Content( text, filename, basename, self ) # Creating a tab object *IMPORTANT* tab.saved = True tab.modified = False self.tab.tabCounter.append(tab.baseName) dirPath = os.path.dirname(filename) self.files = filename # self.tabsOpen.append(self.files) index = self.tab.tabs.addTab( tab, tab.baseName ) # This is the index which we will use to set the current self.tab.tabs.setTabToolTip(index, str(tab.fileName)) if ( not self.dir_opened ): # If a project isn't opened then we open a directory everytime we open a file self.tab.directory.openDirectory(dirPath) self.tab.showDirectory() else: pass self.tab.setLayout(self.tab.layout) # Finally we set the layout update_previous_file(filename) self.tab.tabs.setCurrentIndex( index ) # Setting the index so we could find the current widget self.currentTab = self.tab.tabs.currentWidget() if self.pic_opened is not True: self.currentTab.editor.setFont(self.font) # Setting the font self.currentTab.editor.setFocus() # Setting focus to the tab after we open it self.pic_opened = False except ( IsADirectoryError, AttributeError, UnboundLocalError, PermissionError, ) as E: print(E, " on line 346 in the file main.py") def newFile(self): text = "" if self._dir: base_file_name = "Untitled_file_" + str(random.randint(1, 100)) + ".py" fileName = str(self._dir) + "/" + base_file_name else: base_file_name = "Untitled_file_" + str(random.randint(1, 100)) + ".py" current = os.getcwd() fileName = current + "/" + base_file_name self.pyFileOpened = True # Creates a new blank file file = Content(text, fileName, base_file_name, self) self.tab.splitterH.addWidget( self.tab.tabs ) # Adding tabs, now the directory tree will be on the left self.tab.tabCounter.append(file.fileName) self.tab.setLayout(self.tab.layout) # Finally we set the layout index = self.tab.tabs.addTab( file, file.baseName ) # addTab method returns an index for the tab that was added self.tab.tabs.setTabToolTip(index, str(file.fileName)) self.tab.tabs.setCurrentIndex( index ) # Setting focus to the new tab that we created widget = self.tab.tabs.currentWidget() def newProjectFolder(self): self.dialog = NewProject(self) self.dialog.show() def openProject(self): self._dir = QFileDialog.getExistingDirectory( None, "Select a folder:", "", QFileDialog.ShowDirsOnly ) self.tab.directory.openDirectory(self._dir) self.dir_opened = True # Generating tags file self.generateTagFile(self._dir) self.tab.showDirectory() def generateTagFile(self, directoryLocation: str) -> bool: location = shutil.which("ctags") appDir = os.getcwd() if location is None: print("Please download universal ctags from the website https://github.com/universal-ctags/ctags") return False else: os.chdir(directoryLocation) generateProcess = QProcess(self) command = [location, "-R"] generateProcess.start(" ".join(command)) self.tagInfo.setText("Generating tags file...") self.status.addWidget(self.tagInfo, Qt.AlignRight) generateProcess.finished.connect(lambda: self.afterTagGeneration(appDir)) def afterTagGeneration(self, appDir: str) -> None: os.chdir(appDir) print(os.getcwd()) self.status.removeWidget(self.tagInfo) def parseTagFile(self): pass def openProjectWithPath(self, path): self.tab.directory.openDirectory(path) self.dir_opened = True self._dir = path self.tab.showDirectory() def saveFile(self): self.stack.append(1) try: active_tab = self.tab.tabs.currentWidget() if self.tab.tabs.count(): # If a file is already opened # self.save_thread.add_args(active_tab) # self.save_thread.start() active_tab.start_saving() active_tab.saved = True # active_tab.start_from = os.path.getsize(active_tab.fileName) # self.dead_code_thread.add_args(active_tab.editor.toPlainText()) # self.dead_code_thread.start() # TODO: THrow this analyzer into a code analyzer if len(self.stack) > 5: self.dead_code_thread.add_args(active_tab.editor.toPlainText()) self.dead_code_thread.start() # TODO: THrow this analyzer into a code analyzer self.stack = [] active_tab.modified = False """f if active_tab.fileName.endswith(".py"): active_tab.editor.updateAutoComplete(active_tab.fileName) """ else: options = QFileDialog.Options() name = QFileDialog.getSaveFileName( self, "Save File", "", "All Files (*);;Python Files (*.py);;Text Files (*.txt)", options=options, ) fileName = name[0] with open(fileName, "w+") as saveFile: active_tab.saved = True active_tab.modified = False # self.tabsOpen.append(fileName) saveFile.write(active_tab.editor.toPlainText()) self.tab.events.look_for_dead_code(active_tab.editor.toPlainText()) saveFile.close() """ if fileName.endswith(".py"): active_tab.editor.updateAutoComplete(active_tab.fileName) """ self.setWindowTitle("Hydra ~ " + str(active_tab.baseName) + " [SAVED]") active_tab.tokenize_file() except Exception as E: print(E, " on line 403 in the file main.py") def choose_python(self): return sys.executable def saveFileAs(self): try: active_tab = self.tab.tabs.currentWidget() if active_tab is not None: active_index = self.tab.tabs.currentIndex() options = QFileDialog.Options() name = QFileDialog.getSaveFileName( self, "Save File", "", "All Files (*);;Python Files (*.py);;Text Files (*.txt)", options=options, ) fileName = name[0] with open(fileName, "w+") as saveFile: active_tab.saved = True active_tab.modified = False # self.tabsOpen.append(fileName) try: baseName = os.path.basename(fileName) except AttributeError: print("All tabs closed") saveFile.write(active_tab.editor.toPlainText()) text = active_tab.editor.toPlainText() newTab = Content(str(text), fileName, baseName, self) newTab.ready = True self.tab.tabs.removeTab( active_index ) # When user changes the tab name we make sure we delete the old one index = self.tab.tabs.addTab( newTab, newTab.baseName ) # And add the new one! self.tab.tabs.setTabToolTip(index, str(newTab.fileName)) self.tab.tabs.setCurrentIndex(index) newActiveTab = self.tab.tabs.currentWidget() newActiveTab.editor.setFont(self.font) newActiveTab.editor.setFocus() saveFile.close() self.setWindowTitle("Hydra ~ " + str(active_tab.baseName) + " [SAVED]") else: print("No file opened") except FileNotFoundError: print("File dialog closed") def realterminal(self): """ Checking if the file executing widget already exists in the splitter layout: If it does exist, then we're going to replace the widget with the terminal widget, if it doesn't exist then just add the terminal widget to the layout and expand the splitter. """ if self.tab.splitterV.indexOf(self.tab.Console) == 1: self.tab.splitterV.replaceWidget( self.tab.splitterV.indexOf(self.tab.Console), self.tab.terminal ) self.tab.splitterV.setSizes([400, 10]) else: self.tab.showConsole() def open_documentation(self, data, word): """ Opens documentation for a built in function """ data = data.replace("|", "") index = self.tab.tabs.addTab( Content( data, os.getcwd() + "/" + str(word) + ".doc", str(word) + ".doc", self, True, ), str(word), ) self.tab.tabs.setCurrentIndex(index) def execute_file(self): """ Checking if the terminal widget already exists in the splitter layout: If it does exist, then we're going to replace it, if it doesn't then we're just gonna add our file executer to the layout, expand the splitter and run the file. Then check if the file executer already exists, but is called again to run the file again """ active_tab = self.tab.tabs.currentWidget() python_command = self.choose_python() if self.tab.splitterV.indexOf(self.tab.terminal) == 1: self.tab.splitterV.replaceWidget( self.tab.splitterV.indexOf(self.tab.terminal), self.tab.Console ) self.tab.Console.run( "{} ".format(python_command) + active_tab.fileName, active_tab.fileName ) self.tab.splitterV.setSizes([400, 10]) elif self.tab.splitterV.indexOf(self.tab.Console) == 1: self.tab.Console.run( "{} ".format(python_command) + active_tab.fileName, active_tab.fileName ) self.tab.splitterV.setSizes([400, 10]) else: self.tab.showFileExecuter() self.tab.Console.run( "{} ".format(python_command) + active_tab.fileName, active_tab.fileName ) self.tab.splitterV.setSizes([400, 10]) def jumpToDef(self, tagList: list): print(tagList) tagInfo = tagList[0] fileName = tagList[1] searchCommand = tagList[2] self.cleanOpen(fileName, False, searchCommand) class UpdateThread(QThread): textSignal = pyqtSignal(str) def __init_(self): super().__init__() def run(self): self.textSignal.emit(show_update()) def launch(): # from utils.install_punkt import install_punkt # install_punkt() app = QApplication(sys.argv) try: file = sys.argv[1] except IndexError: # File not given file = get_last_file() app.setStyle("Fusion") palette = QPalette() editor = configs[choiceIndex]["editor"] ex = Main(app, palette, editor) palette.setColor(QPalette.Window, QColor(editor["windowColor"])) palette.setColor(QPalette.WindowText, QColor(editor["windowText"])) palette.setColor(QPalette.Base, QColor(editor["editorColor"])) palette.setColor(QPalette.AlternateBase, QColor(editor["alternateBase"])) palette.setColor(QPalette.ToolTipBase, QColor(editor["ToolTipBase"])) palette.setColor(QPalette.ToolTipText, QColor(editor["ToolTipText"])) palette.setColor(QPalette.Text, QColor(editor["editorText"])) palette.setColor(QPalette.Button, QColor(editor["buttonColor"])) palette.setColor(QPalette.ButtonText, QColor(editor["buttonTextColor"])) palette.setColor(QPalette.Highlight, QColor(editor["HighlightColor"]).lighter()) palette.setColor(QPalette.HighlightedText, QColor(editor["HighlightedTextColor"])) app.setPalette(palette) app.setStyleSheet(material_blue) # uncomment this to have a material blue theme ex.show() if file is not None: ex.cleanOpen(file) ex.openProjectWithPath(os.getcwd()) sys.exit(app.exec_()) if __name__ == "__main__": launch()