diff --git a/examples/integrated/live.qml b/examples/integrated/live.qml index 37a7011..0be8fa6 100644 --- a/examples/integrated/live.qml +++ b/examples/integrated/live.qml @@ -1,9 +1,9 @@ -import QtQuick 2.9 -import QtQuick.Controls 2.2 -import QtQuick.Layouts 1.3 -import QtQuick.Window 2.0 -import Qt.labs.settings 1.0 -import livecoding 1.0 +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QtQuick.Window +import Qt.labs.settings +import livecoding ApplicationWindow { id: root diff --git a/examples/integrated/main.py b/examples/integrated/main.py index 157272e..3a7b6d0 100644 --- a/examples/integrated/main.py +++ b/examples/integrated/main.py @@ -4,9 +4,9 @@ import signal import argparse -from PyQt5.QtGui import QGuiApplication -from PyQt5.QtCore import QObject, QTimer -from PyQt5.QtQml import QQmlApplicationEngine +from qtpy.QtGui import QGuiApplication +from qtpy.QtCore import QObject, QTimer +from qtpy.QtQml import QQmlApplicationEngine from livecoding import start_livecoding_gui @@ -52,6 +52,8 @@ def _start_check_timer(self): args = parser.parse_args() app = QGuiApplication(sys.argv) + app.setOrganizationName('machinekoder.com') + app.setOrganizationDomain('machinekoder.com') gui = MyApp(live=args.live) diff --git a/examples/integrated/main.qml b/examples/integrated/main.qml index 51b0a7c..e7cf8d2 100644 --- a/examples/integrated/main.qml +++ b/examples/integrated/main.qml @@ -1,6 +1,6 @@ -import QtQuick 2.0 -import QtQuick.Controls 2.0 -import myapp 1.0 +import QtQuick +import QtQuick.Controls +import myapp ApplicationWindow { id: root diff --git a/examples/integrated/myapp/MainPanel.qml b/examples/integrated/myapp/MainPanel.qml index 886ed65..ed47728 100644 --- a/examples/integrated/myapp/MainPanel.qml +++ b/examples/integrated/myapp/MainPanel.qml @@ -1,4 +1,4 @@ -import QtQuick 2.0 +import QtQuick Item { id: root diff --git a/examples/standalone/MainScreen.qml b/examples/standalone/MainScreen.qml index c6ffbbb..43b40dc 100644 --- a/examples/standalone/MainScreen.qml +++ b/examples/standalone/MainScreen.qml @@ -1,8 +1,8 @@ -import QtQuick 2.0 -import QtQuick.Controls 2.0 -import QtQuick.Layouts 1.0 -import QtQuick.Window 2.0 -import example.module 1.0 +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QtQuick.Window +import example.module Item { id: root diff --git a/examples/standalone/module/__init__.py b/examples/standalone/module/__init__.py index 709d0bf..8e3b4e6 100644 --- a/examples/standalone/module/__init__.py +++ b/examples/standalone/module/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from qtpy.QtQml import qmlRegisterType from .calculator import Calculator diff --git a/examples/standalone/module/calculator.py b/examples/standalone/module/calculator.py index 563f3d9..65944b2 100644 --- a/examples/standalone/module/calculator.py +++ b/examples/standalone/module/calculator.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from qtpy.QtCore import QObject, Signal, Property diff --git a/src/livecoding/FileSelectionDialog.qml b/src/livecoding/FileSelectionDialog.qml index 1796e3a..1a3ae05 100644 --- a/src/livecoding/FileSelectionDialog.qml +++ b/src/livecoding/FileSelectionDialog.qml @@ -1,7 +1,7 @@ -import QtQuick 2.0 -import QtQuick.Controls 2.0 -import QtQuick.Layouts 1.3 -import Qt.labs.settings 1.0 +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Qt.labs.settings Item { property bool selected: false @@ -16,7 +16,7 @@ Item { id: d readonly property var filteredModel: filterModel(root.model) - function filterModel(model) { + function filterModel(model: list) { var newModel = [] for (var key in model) { var item = model[key] @@ -27,7 +27,7 @@ Item { return newModel } - function select(file) { + function select(file: string) { root.file = "file://" + file root.folder = "file://" + new String(file).substring( 0, file.lastIndexOf('/')) diff --git a/src/livecoding/LiveCodingPanel.qml b/src/livecoding/LiveCodingPanel.qml index 5f51012..e43e08d 100644 --- a/src/livecoding/LiveCodingPanel.qml +++ b/src/livecoding/LiveCodingPanel.qml @@ -1,9 +1,9 @@ -import QtQuick 2.0 -import QtQuick.Controls 2.0 -import QtQuick.Layouts 1.3 -import QtQuick.Window 2.0 -import Qt.labs.settings 1.0 -import livecoding 1.0 +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QtQuick.Window +import Qt.labs.settings +import livecoding Item { id: root @@ -12,7 +12,7 @@ Item { property int visibility: Window.AutomaticVisibility readonly property var defaultNameFilters: ["*.qmlc", "*.jsc", "*.pyc", ".#*", ".*", "*~", "__pycache__", "*___jb_tmp___", // PyCharm safe write - "*___jb_old___"] + "*___jb_old___", ".venv"] property var additionalNameFilters: [] QtObject { @@ -83,9 +83,9 @@ Item { propagateComposedEvents: true visible: false - onClicked: mouse.accepted = false - onPressed: mouse.accepted = false - onReleased: mouse.accepted = false + onClicked: (mouse) => {mouse.accepted = false} + onPressed: (mouse) => {mouse.accepted = false} + onReleased: (mouse) => {mouse.accepted = false} onExited: visible = false onVisibleChanged: delayTimer.start() @@ -202,7 +202,7 @@ Item { FileSelectionDialog { id: fileDialog anchors.fill: parent - model: browser.qmlFiles + model: browser ? browser.qmlFiles : [] visible: !contentItem.loaded onSelectedChanged: { @@ -222,7 +222,7 @@ Item { FileWatcher { id: fileWatcher - fileUrl: browser.projectPath + fileUrl: browser ? browser.projectPath : "" recursive: true enabled: fileDialog.selected onFileChanged: { diff --git a/src/livecoding/filewatcher.py b/src/livecoding/filewatcher.py index 6e46174..b6baf7f 100644 --- a/src/livecoding/filewatcher.py +++ b/src/livecoding/filewatcher.py @@ -22,12 +22,13 @@ class FileWatcher(QObject): fileChanged = Signal() def __init__(self, parent=None): - super(FileWatcher, self).__init__(parent) + super().__init__(parent) self._file_url = QUrl() self._enabled = True self._recursive = False self._name_filters = [] + self._watched_paths = set() self._file_system_watcher = QFileSystemWatcher() @@ -35,7 +36,9 @@ def __init__(self, parent=None): self.enabledChanged.connect(self._update_watched_file) self.recursiveChanged.connect(self._update_watched_file) self.nameFiltersChanged.connect(self._update_watched_file) - self._file_system_watcher.fileChanged.connect(self._on_watched_file_changed) + self._file_system_watcher.fileChanged.connect( + self._on_watched_file_changed + ) self._file_system_watcher.directoryChanged.connect( self._on_watched_directory_changed ) @@ -87,11 +90,9 @@ def nameFilters(self, value): self.nameFiltersChanged.emit(value) def _update_watched_file(self): - files = self._file_system_watcher.files() - if files: + if files := self._file_system_watcher.files(): self._file_system_watcher.removePaths(files) - directories = self._file_system_watcher.directories() - if directories: + if directories := self._file_system_watcher.directories(): self._file_system_watcher.removePaths(directories) if not self._file_url.isValid() or not self._enabled: @@ -105,34 +106,40 @@ def _update_watched_file(self): if local_file == '': return False - if self._recursive and os.path.isdir(local_file): - new_paths = {local_file} - self._file_system_watcher.addPath(local_file) - - it = QDirIterator( - local_file, QDirIterator.Subdirectories | QDirIterator.FollowSymlinks + if os.path.isdir(local_file): + changed, self._watched_paths = self._update_watched_directory( + local_file, self._watched_paths ) - while it.hasNext(): - filepath = it.next() - filename = os.path.basename(filepath) - filtered = False - for wildcard in self._name_filters: - if fnmatch(filename, wildcard): - filtered = True - break - if filename == '..' or filename == '.' or filtered: - continue - self._file_system_watcher.addPath(filepath) - new_paths.add(filepath) - - return new_paths != set(files).union(set(directories)) + return changed elif os.path.exists(local_file): self._file_system_watcher.addPath(local_file) + return True else: qWarning('File to watch does not exist') - return False + return False + + def _update_watched_directory(self, local_file, watched_paths): + new_paths = {local_file} + self._file_system_watcher.addPath(local_file) + + options = QDirIterator.FollowSymlinks + if self._recursive: + options |= QDirIterator.Subdirectories + it = QDirIterator(local_file, options) + while it.hasNext(): + filepath = it.next() + filename = os.path.basename(filepath) + filtered = any( + fnmatch(filename, wildcard) for wildcard in self._name_filters + ) + if filename == '..' or filename == '.' or filtered: + continue + self._file_system_watcher.addPath(filepath) + new_paths.add(filepath) + + return new_paths != watched_paths, new_paths def _on_watched_file_changed(self): if self._enabled: diff --git a/src/livecoding/gui.py b/src/livecoding/gui.py index 421b10f..4afce89 100644 --- a/src/livecoding/gui.py +++ b/src/livecoding/gui.py @@ -16,6 +16,14 @@ MODULE_PATH = os.path.dirname(os.path.abspath(__file__)) +class Global: + def __init__(self): + self.reloader = None + + +g = Global() + + def start_livecoding_gui(engine, project_path, main_file, live_qml=''): """ Starts the live coding GUI. @@ -28,9 +36,8 @@ def start_livecoding_gui(engine, project_path, main_file, live_qml=''): register_types() recursively_register_types(project_path) - global reloader # necessary to make reloading work, prevents garbage collection - reloader = PythonReloader(main_file) - engine.rootContext().setContextProperty(PythonReloader.__name__, reloader) + g.reloader = PythonReloader(main_file) + engine.rootContext().setContextProperty(PythonReloader.__name__, g.reloader) engine.rootContext().setContextProperty( 'userProjectPath', QUrl.fromLocalFile(project_path) ) diff --git a/src/livecoding/live.qml b/src/livecoding/live.qml index bb58ea3..f8cef94 100644 --- a/src/livecoding/live.qml +++ b/src/livecoding/live.qml @@ -1,9 +1,9 @@ -import QtQuick 2.9 -import QtQuick.Controls 2.2 -import QtQuick.Layouts 1.3 -import QtQuick.Window 2.0 -import Qt.labs.settings 1.0 -import livecoding 1.0 +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QtQuick.Window +import Qt.labs.settings +import livecoding ApplicationWindow { id: root diff --git a/src/livecoding/moduleloader.py b/src/livecoding/moduleloader.py index 1553166..853ae51 100644 --- a/src/livecoding/moduleloader.py +++ b/src/livecoding/moduleloader.py @@ -1,9 +1,7 @@ -# -*- coding: utf-8 -*- import os import sys -from importlib import import_module -from six.moves import reload_module +from importlib import import_module, reload def recursively_register_types(root_path): @@ -11,26 +9,33 @@ def recursively_register_types(root_path): sys.path.insert(0, root_path) for root, dirs, files in os.walk(root_path): + if '.venv' in root or '__pycache__' in root: + continue for file in files: - if file != '__init__.py': + _, ext = os.path.splitext(file) + if ext != '.py': continue + path = os.path.join(root, file) with open(path, 'rt') as f: data = f.read() - if 'def register_types()' not in data: - continue - _register_module(path, root_path) + if ('QML_IMPORT_NAME' in data and 'QML_IMPORT_MAJOR_VERSION' in data) or ( + file == '__init__.py' and 'def register_types()' in data + ): + _register_module(path, root_path) def _register_module(file_path, root_path): path = os.path.relpath(file_path, root_path) - name = os.path.dirname(path).replace('/', '.') + path = path[:-3] if path.endswith('.py') else os.path.dirname(path) + name = path.replace('/', '.') try: if name in sys.modules: - reload_module(sys.modules[name]) + reload(sys.modules[name]) module = sys.modules[name] else: module = import_module(name) - module.register_types() + if hasattr(module, 'register_types'): + module.register_types() except Exception as e: - print('Error importing %s: %s' % (name, e)) + print(f'Error importing {name}: {e}') diff --git a/src/livecoding/projectbrowser.py b/src/livecoding/projectbrowser.py index 0778f76..d9f8fbe 100644 --- a/src/livecoding/projectbrowser.py +++ b/src/livecoding/projectbrowser.py @@ -2,6 +2,7 @@ import os from qtpy.QtCore import QObject, Property, Signal, QUrl, QDir, Slot +from qtpy.QtQml import QJSValue class ProjectBrowser(QObject): @@ -33,16 +34,17 @@ def projectPath(self, value): self._project_path = value self.projectPathChanged.emit(value) - @Property('QStringList', notify=qmlFilesChanged) + @Property('QVariant', notify=qmlFilesChanged) def qmlFiles(self): return self._qml_files - @Property('QStringList', notify=extensionsChanged) + @Property('QVariant', notify=extensionsChanged) def extensions(self): return self._extensions @extensions.setter - def extensions(self, value): + def extensions(self, value: QJSValue): + value = value.toVariant() if self._extensions == value: return self._extensions = value @@ -63,7 +65,7 @@ def _update_files(self): # convert file separators to consistent style url = QUrl.fromLocalFile(path).toLocalFile() if not url.startswith('/'): - url = '/' + url + url = f'/{url}' file_list.append(url) self._qml_files = file_list self.qmlFilesChanged.emit() diff --git a/src/livecoding/pythonreloader.py b/src/livecoding/pythonreloader.py index 6f33ee5..4326c56 100644 --- a/src/livecoding/pythonreloader.py +++ b/src/livecoding/pythonreloader.py @@ -17,10 +17,9 @@ def restart(self): import_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), '..') python_path = os.environ.get('PYTHONPATH', '') if import_dir not in python_path: - python_path += ':{}'.format(import_dir) + python_path += f':{import_dir}' os.environ['PYTHONPATH'] = python_path args = [sys.executable, self._main] + sys.argv[1:] - handler = signal.getsignal(signal.SIGTERM) - if handler: + if handler := signal.getsignal(signal.SIGTERM): handler(signal.SIGTERM, inspect.currentframe()) os.execv(sys.executable, args) diff --git a/src/livecoding/tests/test_filewatcher.py b/src/livecoding/tests/test_filewatcher.py index f42945d..eafc258 100644 --- a/src/livecoding/tests/test_filewatcher.py +++ b/src/livecoding/tests/test_filewatcher.py @@ -17,7 +17,7 @@ def watcher(): def test_creating_and_writing_file_in_directory_emits_signal(qtbot, tmpdir, watcher): - watcher.fileUrl = QUrl('file://' + str(tmpdir)) + watcher.fileUrl = QUrl(f'file://{str(tmpdir)}') watcher.enabled = True watcher.recursive = True spy = QSignalSpy(watcher.fileChanged) @@ -26,28 +26,28 @@ def test_creating_and_writing_file_in_directory_emits_signal(qtbot, tmpdir, watc f.write('foo') spy.wait(SIGNAL_WAIT_TIMEOUT) - assert len(spy) == 1 + assert spy.count() == 1 def test_changing_file_emits_signal(qtbot, tmpdir, watcher): f = tmpdir.join('test.txt') f.write('foo') watcher.recursive = False - watcher.fileUrl = QUrl('file://' + str(f)) + watcher.fileUrl = QUrl(f'file://{str(f)}') watcher.enabled = True spy = QSignalSpy(watcher.fileChanged) f.write('bar') spy.wait(SIGNAL_WAIT_TIMEOUT) - assert len(spy) == 1 + assert spy.count() == 1 def test_creating_and_writing_file_on_filter_list_doesnt_emit_signal( qtbot, tmpdir, watcher ): watcher.nameFilters = ['.#*'] - watcher.fileUrl = QUrl('file://' + str(tmpdir)) + watcher.fileUrl = QUrl(f'file://{str(tmpdir)}') watcher.enabled = True watcher.recursive = True spy = QSignalSpy(watcher.fileChanged) @@ -56,35 +56,35 @@ def test_creating_and_writing_file_on_filter_list_doesnt_emit_signal( f.write('foo') spy.wait(SIGNAL_WAIT_TIMEOUT) - assert len(spy) == 0 + assert spy.count() == 0 def test_renaming_file_emits_signal(qtbot, tmpdir, watcher): f = tmpdir.join('supp') f.write('pncp0A') watcher.recursive = True - watcher.fileUrl = QUrl('file://' + str(tmpdir)) + watcher.fileUrl = QUrl(f'file://{str(tmpdir)}') watcher.enabled = True spy = QSignalSpy(watcher.fileChanged) os.rename(str(f), os.path.join(str(tmpdir), 'energist')) spy.wait(SIGNAL_WAIT_TIMEOUT) - assert len(spy) > 0 + assert spy.count() > 0 def test_deleting_file_emits_signal(qtbot, tmpdir, watcher): f = tmpdir.join('lowered') f.write('pncp0A') watcher.recursive = True - watcher.fileUrl = QUrl('file://' + str(tmpdir)) + watcher.fileUrl = QUrl(f'file://{str(tmpdir)}') watcher.enabled = True spy = QSignalSpy(watcher.fileChanged) os.remove(str(f)) spy.wait(SIGNAL_WAIT_TIMEOUT) - assert len(spy) == 1 + assert spy.count() == 1 def test_deleting_directory_emits_signal(qtbot, tmpdir, watcher): @@ -92,20 +92,20 @@ def test_deleting_directory_emits_signal(qtbot, tmpdir, watcher): f = subdir.join("yeasts") f.write("Wlb2Msh") # need to create a file inside the tmpdir to force creation watcher.recursive = True - watcher.fileUrl = QUrl('file://' + str(tmpdir)) + watcher.fileUrl = QUrl(f'file://{str(tmpdir)}') watcher.enabled = True spy = QSignalSpy(watcher.fileChanged) shutil.rmtree(str(subdir)) spy.wait(SIGNAL_WAIT_TIMEOUT) - assert len(spy) == 1 + assert spy.count() == 1 def test_creating_file_in_subdirectory_emits_signal(qtbot, tmpdir, watcher): subdir = tmpdir.mkdir('sub') watcher.recursive = True - watcher.fileUrl = QUrl('file://' + str(tmpdir)) + watcher.fileUrl = QUrl(f'file://{str(tmpdir)}') watcher.enabled = True spy = QSignalSpy(watcher.fileChanged) @@ -113,4 +113,4 @@ def test_creating_file_in_subdirectory_emits_signal(qtbot, tmpdir, watcher): f.write('DNsqu') spy.wait(SIGNAL_WAIT_TIMEOUT) - assert len(spy) == 1 + assert spy.count() == 1