from utils import log, open_folder_in_explorer import os import re import glob import sys import codecs import platform import requests import validators from PySide import QtGui, QtCore from PySide.QtGui import QApplication, QHBoxLayout, QVBoxLayout from PySide.QtNetwork import QHttp, QHttpRequestHeader from PySide.QtCore import QUrl, QFile, QIODevice, QCoreApplication from pycns import pngs_from_icns from command_line import CommandBase, logger, get_file from command_line import __version__ as __gui_version__ from utils import get_data_path, get_data_file_path import utils COMMAND_LINE = False MAX_RECENT = 10 def url_exists(path): if validators.url(path): return True return False class ColorDisplay(QtGui.QWidget): colorChanged = QtCore.Signal(QtGui.QColor) def __init__(self, initial, parent=None): super(ColorDisplay, self).__init__(parent) self.color = None self.setColor(initial, False) def setColor(self, color, emit=True): if isinstance(color, (str, bytes)): self.color = QtGui.QColor(color) else: self.color = color self.update() if emit: self.colorChanged.emit(self.color) def paintEvent(self, event=None): painter = QtGui.QPainter(self) if self.color is not None: painter.setBrush(QtGui.QBrush(self.color)) rect = self.rect() margins = self.contentsMargins() new_rect = QtCore.QRect(rect.left()+margins.left(), rect.top()+margins.top(), rect.width()-margins.right()*2, rect.height()-margins.bottom()*2) painter.drawRect(new_rect) def getColorName(self): return str(self.color.name()) class ExistingProjectDialog(QtGui.QDialog): def __init__(self, recent_projects, directory_callback, parent=None): super(ExistingProjectDialog, self).__init__(parent) self.setWindowTitle('Open Project Folder') self.setWindowIcon(QtGui.QIcon(get_file('files/images/icon.png'))) self.setMinimumWidth(500) group_box = QtGui.QGroupBox('Existing Projects') gbox_layout = QtGui.QVBoxLayout() self.project_list = QtGui.QListWidget() gbox_layout.addWidget(self.project_list) group_box.setLayout(gbox_layout) self.callback = directory_callback self.projects = recent_projects for i in range(len(recent_projects)): project = recent_projects[i] text = u'{} - {}'.format(os.path.basename(project), project) self.project_list.addItem(text) self.project_list.itemClicked.connect(self.project_clicked) self.cancel = QtGui.QPushButton('Cancel') self.open = QtGui.QPushButton('Open Selected') self.browse = QtGui.QPushButton('Browse...') self.open.setEnabled(False) self.open.clicked.connect(self.open_clicked) self.browse.clicked.connect(self.browse_clicked) buttons = QtGui.QWidget() button_layout = QtGui.QHBoxLayout() button_layout.addWidget(self.cancel) button_layout.addWidget(QtGui.QWidget()) button_layout.addWidget(self.browse) button_layout.addWidget(self.open) buttons.setLayout(button_layout) layout = QtGui.QVBoxLayout() layout.addWidget(group_box) layout.addWidget(buttons) self.setLayout(layout) self.cancel.clicked.connect(self.cancelled) def browse_clicked(self): directory = QtGui.QFileDialog.getExistingDirectory(self, 'Find Project Directory', self.parent().project_dir() or self.parent().last_project_dir) if directory: self.callback(directory) self.close() def open_clicked(self): pos = self.project_list.currentRow() self.callback(self.projects[pos]) self.close() def project_clicked(self, item): self.open.setEnabled(True) def cancelled(self): self.close() class Validator(QtGui.QRegExpValidator): def __init__(self, regex, action, parent=None): self.exp = regex self.action = str if hasattr(str, action): self.action = getattr(str, action) reg = QtCore.QRegExp(regex) super(Validator, self).__init__(reg, parent) def validate(self, text, pos): result = super(Validator, self).validate(text, pos) return result def fixup(self, text): return ''.join(re.findall(self.exp, self.action(text))) class BackgroundThread(QtCore.QThread): def __init__(self, widget, method_name, parent=None): QtCore.QThread.__init__(self, parent) self.widget = widget self.method_name = method_name def run(self): if hasattr(self.widget, self.method_name): func = getattr(self.widget, self.method_name) func() class MainWindow(QtGui.QMainWindow, CommandBase): def update_electron_versions(self, button): self.get_versions_in_background() def load_recent_projects(self): files = [] history_file = get_data_file_path('files/recent_files.txt') if not os.path.exists(history_file): return files with codecs.open(history_file, encoding='utf-8') as f: for line in f: line = line.strip() if line and os.path.exists(line): files.append(line) files.reverse() return files def load_last_project_path(self): proj_path = '' proj_file = get_data_file_path('files/last_project_path.txt') if os.path.exists(proj_file): with codecs.open(proj_file, encoding='utf-8') as f: proj_path = f.read().strip() if not proj_path: proj_path = QtCore.QDir.currentPath() return proj_path def save_project_path(self, path): proj_file = get_data_file_path('files/last_project_path.txt') with codecs.open(proj_file, 'w+', encoding='utf-8') as f: f.write(path) def save_recent_project(self, proj): recent_file_path = get_data_file_path('files/recent_files.txt') max_length = MAX_RECENT recent_files = [] if os.path.exists(recent_file_path): recent_files = codecs.open(recent_file_path, encoding='utf-8').read().split(u'\n') try: recent_files.remove(proj) except ValueError: pass recent_files.append(proj) with codecs.open(recent_file_path, 'w+', encoding='utf-8') as f: for recent_file in recent_files[-max_length:]: if recent_file and os.path.exists(recent_file): f.write(u'{}\n'.format(recent_file)) def update_recent_files(self): previous_files = self.load_recent_projects() self.recent_separator.setVisible(len(previous_files) > 0) for i in range(len(previous_files)): text = u'{} - {}'.format(i+1, os.path.basename(previous_files[i])) action = self.recent_file_actions[i] action.setText(text) action.setData(previous_files[i]) action.setVisible(True) def __init__(self, width, height, app, parent=None): super(MainWindow, self).__init__(parent) CommandBase.__init__(self) recent_projects = self.load_recent_projects() self.existing_dialog = ExistingProjectDialog(recent_projects, self.load_project, parent=self) drect = QtGui.QApplication.desktop().availableGeometry(self) center = drect.center() self.move(center.x() - self.width() * 0.5, center.y() - self.height()*0.5) self.icon_style = 'width:48px;height:48px;background-color:white;border-radius:5px;border:1px solid rgb(50,50,50);' self.last_project_dir = self.load_last_project_path() status_bar = QtGui.QStatusBar() self.setStatusBar(status_bar) self.project_path = '' #if platform.system() == 'Darwin': # self.menuBar().setNativeMenuBar(False) self.project_menu = self.menuBar().addMenu('File') browse_action = QtGui.QAction('Open Project', self.project_menu, shortcut=QtGui.QKeySequence.Open, statusTip='Open an existing or new project.', triggered=self.browse_dir) self.project_menu.addAction(browse_action) self.project_menu.addSeparator() self.recent_file_actions = [] for i in range(MAX_RECENT): if i == 9: key = 0 else: key = i+1 action = QtGui.QAction(self, visible=False, triggered=self.open_recent_file, shortcut=QtGui.QKeySequence('Ctrl+{}'.format(key))) self.recent_file_actions.append(action) self.project_menu.addAction(action) self.recent_separator = self.project_menu.addSeparator() self.update_recent_files() exit_action = QtGui.QAction('Exit', self.project_menu) exit_action.triggered.connect(QtGui.qApp.closeAllWindows) self.project_menu.addAction(exit_action) self.logger = logger self.gui_app = app self.desktop_width = app.desktop().screenGeometry().width() self.desktop_height = app.desktop().screenGeometry().height() self.options_enabled = False self.output_package_json = True self.setWindowIcon(QtGui.QIcon(get_file('files/images/icon.png'))) self.update_json = False self.setup_electron_versions() self.thread = None self.original_packagejson = {} self.resize(width, height) self.extract_error = None self.create_application_layout() self.option_settings_enabled(False) self.setWindowTitle(u"Electrify {}".format(__gui_version__)) self.update_electron_versions(None) def open_recent_file(self): action = self.sender() if action: self.load_project(action.data()) def setup_electron_versions(self): electron_version = self.get_setting('electron_version') try: f = codecs.open(get_data_file_path('files/electron-versions.txt'), encoding='utf-8') for line in f: electron_version.values.append(line.strip()) f.close() except IOError: electron_version.values.append(electron_version.default_value) def create_application_layout(self): self.main_layout = QtGui.QVBoxLayout() self.tab_widget = QtGui.QTabWidget() self.main_layout.setContentsMargins(10, 5, 10, 5) self.create_layout_widgets() self.addWidgets_to_main_layout() w = QtGui.QWidget() w.setLayout(self.main_layout) self.setCentralWidget(w) def create_layout_widgets(self): self.download_bar_widget = self.create_download_bar() self.app_settings_widget = self.create_application_settings() self.comp_settings_widget = self.create_compression_settings() self.win_settings_widget = self.create_window_settings() self.ex_settings_widget = self.create_export_settings() self.web_prefs_widget = self.create_web_prefs_settings() self.dl_settings_widget = self.create_download_settings() self.directory_chooser_widget = self.create_directory_choose() def addWidgets_to_main_layout(self): self.warning_settings_icon = QtGui.QIcon(get_file('files/images/warning.png')) self.app_settings_icon = QtGui.QIcon(get_file('files/images/app_settings.png')) self.web_prefs_icon = QtGui.QIcon(get_file('files/images/web_prefs.png')) self.win_settings_icon = QtGui.QIcon(get_file('files/images/window_settings.png')) self.ex_settings_icon = QtGui.QIcon(get_file('files/images/export_settings.png')) self.comp_settings_icon = QtGui.QIcon(get_file('files/images/compress_settings.png')) self.download_settings_icon = QtGui.QIcon(get_file('files/images/download_settings.png')) self.tab_icons = [self.app_settings_icon, self.win_settings_icon, self.web_prefs_icon, self.ex_settings_icon, self.comp_settings_icon, self.download_settings_icon] self.main_layout.addWidget(self.directory_chooser_widget) self.tab_widget.addTab(self.app_settings_widget, self.app_settings_icon, 'App Settings') self.tab_widget.addTab(self.win_settings_widget, self.win_settings_icon, 'Window Settings') self.tab_widget.addTab(self.web_prefs_widget, self.web_prefs_icon, 'Web Preferences') self.tab_widget.addTab(self.ex_settings_widget, self.ex_settings_icon, 'Export Settings') self.tab_widget.addTab(self.comp_settings_widget, self.comp_settings_icon, 'Compression Settings') self.tab_widget.addTab(self.dl_settings_widget, self.download_settings_icon, 'Download Settings') self.main_layout.addWidget(self.tab_widget) self.main_layout.addLayout(self.download_bar_widget) def option_settings_enabled(self, is_enabled): self.ex_button.setEnabled(is_enabled) self.app_settings_widget.setEnabled(is_enabled) self.win_settings_widget.setEnabled(is_enabled) self.ex_settings_widget.setEnabled(is_enabled) self.web_prefs_widget.setEnabled(is_enabled) self.comp_settings_widget.setEnabled(is_enabled) self.dl_settings_widget.setEnabled(is_enabled) self.options_enabled = is_enabled def export(self, export_button, cancel_button): self.get_files_to_download() self.try_to_download_files() def open_export(self, open_export_button): open_folder_in_explorer(self.output_dir()) def try_to_download_files(self): if self.files_to_download: self.progress_bar.setVisible(True) self.cancel_button.setEnabled(True) self.disable_ui_while_working() self.download_file_with_error_handling() else: # This shouldn't happen since we disable the UI if there are no # options selected # But in the weird event that this does happen, we are prepared! QtGui.QMessageBox.information(self, 'Export Options Empty!', ('Please choose one of ' 'the export options!')) def selected_version(self): return self.get_setting('electron_version').value def enable_ui_after_error(self): self.enable_ui() self.progress_text = '' self.progress_bar.setVisible(False) self.cancel_button.setEnabled(False) def show_error(self, exception): QtGui.QMessageBox.information(self, 'Error!', exception) def disable_ui_while_working(self): self.option_settings_enabled(False) self.directory_chooser_widget.setEnabled(False) def enable_ui(self): self.option_settings_enabled(True) self.directory_chooser_widget.setEnabled(True) def get_tab_index_for_setting_name(self, name): options_dict = {'app_settings': 0, 'webkit_settings': 0, 'window_settings': 1, 'web_preferences': 2, 'export_settings': 3, 'electrify_settings': 3, 'compression': 4, 'download_settings': 5} for setting_group_name, setting_group in self._setting_items: if name in setting_group: return options_dict.get(setting_group_name, None) def required_settings_filled(self, ignore_options=False): if not self.options_enabled and not ignore_options: return False red_border = 'QLineEdit{border:3px solid rgba(238, 68, 83, 200); border-radius:5px;}' settings_valid = True for sgroup in self.settings['setting_groups']+[self.settings['electrify_settings']]: for sname, setting in sgroup.items(): if setting.type in set(['file', 'folder']) and os.path.isabs(setting.value): setting_path = setting.value else: setting_path = utils.path_join(self.project_dir(), setting.value) if setting.required and not setting.value: settings_valid = False widget = self.find_child_by_name(setting.name) if widget is not None: widget.setStyleSheet(red_border) widget.setToolTip('This setting is required.') tab = self.get_tab_index_for_setting_name(setting.name) self.tab_widget.setTabIcon(tab, self.warning_settings_icon) if (setting.type == 'file' and setting.value and setting.exists): setting_path_invalid = not os.path.exists(setting_path) setting_url_invalid = not url_exists(setting.value) if setting_path_invalid and setting_url_invalid: log(setting.value, "does not exist") settings_valid = False widget = self.find_child_by_name(setting.name) if widget is not None: widget.setStyleSheet(red_border) widget.setToolTip(u'The file or url "{}" does not exist.'.format(setting.value)) tab = self.get_tab_index_for_setting_name(setting.name) self.tab_widget.setTabIcon(tab, self.warning_settings_icon) if (setting.type == 'folder' and setting.value and not os.path.exists(setting_path)): settings_valid = False widget = self.find_child_by_name(setting.name) if widget is not None: widget.setStyleSheet(red_border) widget.setToolTip(u'The folder "{}" does not exist'.format(setting_path)) tab = self.get_tab_index_for_setting_name(setting.name) self.tab_widget.setTabIcon(tab, self.warning_settings_icon) if settings_valid: widget = self.find_child_by_name(setting.name) if widget is not None: widget.setStyleSheet('') widget.setToolTip('') tab = self.get_tab_index_for_setting_name(setting.name) self.tab_widget.setTabIcon(tab, self.tab_icons[tab]) export_chosen = False for setting_name, setting in self.settings['export_settings'].items(): if setting.value: export_chosen = True if not settings_valid: return export_chosen and settings_valid for setting_name, setting in self.settings['export_settings'].items(): if not export_chosen: widget = self.find_child_by_name(setting.name) if widget is not None: widget.setStyleSheet('QCheckBox{border:3px solid rgba(238, 68, 83, 200); border-radius:5px;}') widget.setToolTip('At least one of these options should be selected.') tab = self.get_tab_index_for_setting_name(setting.name) self.tab_widget.setTabIcon(tab, self.warning_settings_icon) else: widget = self.find_child_by_name(setting.name) if widget is not None: widget.setStyleSheet('') widget.setToolTip('') tab = self.get_tab_index_for_setting_name(setting.name) self.tab_widget.setTabIcon(tab, self.tab_icons[tab]) return export_chosen and settings_valid def project_dir(self): return self.project_path def output_dir(self): if hasattr(self, 'output_line'): if os.path.isabs(self.output_line.text()): return self.output_line.text() else: return utils.path_join(self.project_dir(), self.output_line.text()) return '' def create_download_bar(self): hlayout = QtGui.QHBoxLayout() vlayout = QtGui.QVBoxLayout() vlayout.setContentsMargins(5, 5, 5, 5) vlayout.setSpacing(5) hlayout.setSpacing(5) hlayout.setContentsMargins(5, 5, 5, 5) progress_label = QtGui.QLabel('') progress_bar = QtGui.QProgressBar() progress_bar.setVisible(False) progress_bar.setContentsMargins(5, 5, 5, 5) vlayout.addWidget(progress_label) vlayout.addWidget(progress_bar) vlayout.addWidget(QtGui.QLabel('')) ex_button = QtGui.QPushButton('Export') ex_button.setEnabled(False) cancel_button = QtGui.QPushButton('Cancel Download') cancel_button.setEnabled(False) open_export_button = QtGui.QPushButton() open_export_button.setEnabled(False) open_export_button.setIcon(QtGui.QIcon(get_file('files/images/folder_open.png'))) open_export_button.setToolTip('Open Export Folder') open_export_button.setStatusTip('Open Export Folder') open_export_button.setMaximumWidth(30) open_export_button.setMaximumHeight(30) ex_button.clicked.connect(self.call_with_object('export', ex_button, cancel_button)) cancel_button.clicked.connect(self.cancel_download) open_export_button.clicked.connect(self.call_with_object('open_export', open_export_button)) button_box = QtGui.QDialogButtonBox() button_box.addButton(open_export_button, QtGui.QDialogButtonBox.NoRole) button_box.addButton(cancel_button, QtGui.QDialogButtonBox.RejectRole) button_box.addButton(ex_button, QtGui.QDialogButtonBox.AcceptRole) hlayout.addLayout(vlayout) hlayout.addWidget(button_box) self.progress_label = progress_label self.progress_bar = progress_bar self.cancel_button = cancel_button self.open_export_button = open_export_button http = QHttp(self) http.sslErrors.connect(self.https_error) http.requestFinished.connect(self.http_request_finished) http.dataReadProgress.connect(self.update_progress_bar) http.responseHeaderReceived.connect(self.read_response_header) self.http = http self.ex_button = ex_button return hlayout def https_error(self, errors): print(errors) def read_response_header(self, response_header): # Check for genuine error conditions. if response_header.statusCode() not in (200, 300, 301, 302, 303, 307): self.show_error(u'Download failed: {}.'.format(response_header.reasonPhrase())) self.http_request_aborted = True self.http.abort() self.enable_ui_after_error() def http_request_finished(self, request_id, error): if request_id != self.http_get_id: return if self.http_request_aborted: if self.out_file is not None: self.out_file.close() self.out_file.remove() self.out_file = None return self.out_file.close() self.http.abort() if error: self.out_file.remove() self.show_error(u'Download failed: {}.'.format(self.http.errorString())) self.enable_ui_after_error() else: self.continue_downloading_or_extract() def continue_downloading_or_extract(self): if self.files_to_download: self.progress_bar.setVisible(True) self.cancel_button.setEnabled(True) self.disable_ui_while_working() self.download_file_with_error_handling() else: self.progress_text = 'Done.' self.cancel_button.setEnabled(False) self.progress_bar.setVisible(False) self.extract_files_in_background() @property def progress_text(self): return self.progress_label.text() @progress_text.setter def progress_text(self, value): self.progress_label.setText(value) def run_in_background(self, method_name, callback): self.thread = BackgroundThread(self, method_name) self.thread.finished.connect(callback) self.thread.start() def get_versions_in_background(self): self.ex_button.setEnabled(False) self.run_in_background('get_versions', self.done_getting_versions) def done_getting_versions(self): self.ex_button.setEnabled(self.required_settings_filled()) self.progress_text = 'Done retrieving versions.' electron_version = self.get_setting('electron_version') combo = self.find_child_by_name(electron_version.name) combo.clear() combo.addItems(electron_version.values) def make_output_files_in_background(self): self.ex_button.setEnabled(False) self.run_in_background('make_output_dirs', self.done_making_files) def run_custom_script(self): script = self.get_setting('custom_script').value self.run_script(script) def script_done(self): self.ex_button.setEnabled(self.required_settings_filled()) self.enable_ui() self.progress_text = 'Done!' def done_making_files(self): self.ex_button.setEnabled(self.required_settings_filled()) self.progress_text = 'Done Exporting.' self.delete_files() if self.output_err: self.show_error(self.output_err) self.enable_ui_after_error() else: self.progress_text = 'Running custom script...' self.ex_button.setEnabled(False) self.run_in_background('run_custom_script', self.script_done) def extract_files_in_background(self): self.progress_text = 'Extracting.' self.ex_button.setEnabled(False) self.run_in_background('extract_files', self.done_extracting) def done_extracting(self): self.ex_button.setEnabled(self.required_settings_filled()) if self.extract_error: self.progress_text = 'Error extracting.' self.show_error('There were one or more errors with your ' 'zip/tar files. They were deleted. Please ' 'try to export again.') self.enable_ui_after_error() else: self.progress_text = 'Done extracting.' self.make_output_files_in_background() def cancel_download(self): self.progress_text = 'Download cancelled.' self.cancel_button.setEnabled(False) self.http_request_aborted = True self.http.abort() self.enable_ui() self.progress_bar.setValue(0) self.progress_bar.setVisible(False) def update_progress_bar(self, bytes_read, total_bytes): if self.http_request_aborted: self.progress_bar.setValue(0) self.progress_bar.setVisible(False) return self.progress_bar.setMaximum(total_bytes) self.progress_bar.setValue(bytes_read) def download_file(self, path, setting): version_file = self.settings['base_url'].format(self.selected_version()) location = self.get_setting('download_dir').value versions = re.findall('v(\d+)\.(\d+)\.(\d+)', path)[0] self.progress_text = u'Downloading {}'.format(path.replace(version_file, '')) path = self.get_redirected_url(path) url = QUrl(path) file_name = setting.save_file_path(self.selected_version(), location) archive_exists = QFile.exists(file_name) forced = self.get_setting('force_download').value if archive_exists and not forced: self.continue_downloading_or_extract() return self.out_file = QFile(file_name) if not self.out_file.open(QIODevice.WriteOnly): error = self.out_file.error().name self.show_error(u'Unable to save the file {}: {}.'.format(file_name, error)) self.out_file = None self.enable_ui() return mode = QHttp.ConnectionModeHttps port = url.port() if port == -1: port = 0 self.http.setHost(url.host(), mode, port) self.http_request_aborted = False # Download the file. self.http_get_id = self.http.get(path, to=self.out_file) def create_icon_box(self, name, text): style = 'width:48px;height:48px;background-color:white;border-radius:5px;border:1px solid rgb(50,50,50);' icon_label = QtGui.QLabel() icon_label.setStyleSheet(style) icon_label.setMaximumWidth(48) icon_label.setMinimumWidth(48) icon_label.setMaximumHeight(48) icon_label.setMinimumHeight(48) setattr(self, name, icon_label) icon_text = QtGui.QLabel(text) icon_text.setStyleSheet('font-size:10px;') icon_text.setAlignment(QtCore.Qt.AlignCenter) vbox = QVBoxLayout() vbox.setAlignment(QtCore.Qt.AlignCenter) vbox.addWidget(icon_label) vbox.addWidget(icon_text) vbox.setContentsMargins(0, 0, 0, 0) w = QtGui.QWidget() w.setLayout(vbox) w.setMaximumWidth(70) return w def create_directory_choose(self): group_box = QtGui.QGroupBox('An awesome web project called:') title_hbox = QHBoxLayout() title_hbox.setContentsMargins(10, 10, 10, 10) win_icon = self.create_icon_box('window_icon', 'Window Icon') exe_icon = self.create_icon_box('exe_icon', 'Exe Icon') mac_icon = self.create_icon_box('mac_icon', 'Mac Icon') self.title_label = QtGui.QLabel('TBD') self.title_label.setStyleSheet('font-size:20px; font-weight:bold;') title_hbox.addWidget(self.title_label) title_hbox.addWidget(QtGui.QLabel()) title_hbox.addWidget(win_icon) title_hbox.addWidget(exe_icon) title_hbox.addWidget(mac_icon) vlayout = QtGui.QVBoxLayout() vlayout.setSpacing(5) vlayout.setContentsMargins(10, 5, 10, 5) vlayout.addLayout(title_hbox) #vlayout.addLayout(input_layout) #vlayout.addLayout(output_layout) group_box.setLayout(vlayout) return group_box def set_window_icon(self): icon_setting = self.get_setting('icon') mac_icon_setting = self.get_setting('mac_icon') exe_icon_setting = self.get_setting('exe_icon') self.set_icon(icon_setting.value, self.window_icon) if not mac_icon_setting.value: self.set_icon(icon_setting.value, self.mac_icon) if not exe_icon_setting.value: self.set_icon(icon_setting.value, self.exe_icon) def set_exe_icon(self): icon_setting = self.get_setting('exe_icon') self.set_icon(icon_setting.value, self.exe_icon) def set_mac_icon(self): icon_setting = self.get_setting('mac_icon') self.set_icon(icon_setting.value, self.mac_icon) def set_icon(self, icon_path, icon): if icon_path: icon_path = utils.path_join(self.project_dir(), icon_path) if os.path.exists(icon_path): if icon_path.endswith('.icns'): pngs = pngs_from_icns(icon_path) if pngs: ba = QtCore.QByteArray(pngs[-1].data) image = QtGui.QImage.fromData(ba, 'PNG') else: return else: image = QtGui.QImage(icon_path) if image.width() >= image.height(): image = image.scaledToWidth(48, QtCore.Qt.SmoothTransformation) else: image = image.scaledToHeight(48, QtCore.Qt.SmoothTransformation) icon.setPixmap(QtGui.QPixmap.fromImage(image)) icon.setStyleSheet('') else: icon.setPixmap(None) icon.setStyleSheet(self.icon_style) else: icon.setPixmap(None) icon.setStyleSheet(self.icon_style) def call_with_object(self, name, obj, *args, **kwargs): """Allows arguments to be passed to click events""" def call(*cargs, **ckwargs): if hasattr(self, name): func = getattr(self, name) kwargs.update(ckwargs) func(obj, *(args+cargs), **kwargs) return call def find_child_by_name(self, name): return self.findChild(QtCore.QObject, name) def find_all_children(self, names): children = [] for child in self.find_children(QtCore.QObject): if child.object_name() in names: children.append(child) return children def project_name(self): return self.find_child_by_name('app_name').text() def browse_dir(self): directory = QtGui.QFileDialog.getExistingDirectory(self, 'Find Project Directory', self.project_dir() or self.last_project_dir) if directory: self.load_project(directory) def load_project(self, directory): self.update_json = False self.project_path = directory self.save_recent_project(directory) self.save_project_path(directory) self.update_recent_files() self.reset_settings() #self.input_line.setText(directory) proj_name = os.path.basename(directory) self.title_label.setText(proj_name) setting_input = self.find_child_by_name('main_html') files = (glob.glob(utils.path_join(directory, 'index.html')) + glob.glob(utils.path_join(directory, 'index.php')) + glob.glob(utils.path_join(directory, 'index.htm'))) if not setting_input.text(): if files: setting_input.setText(files[0].replace(self.project_dir() + os.path.sep, '')) app_name_input = self.find_child_by_name('app_name') title_input = self.find_child_by_name('title') if not app_name_input.text(): app_name_input.setText(proj_name) if not title_input.text(): title_input.setText(proj_name) self.load_package_json(utils.get_data_file_path('files/global.json')) self.load_package_json() default_dir = 'output' export_dir_setting = self.get_setting('export_dir') default_dir = export_dir_setting.value or default_dir self.output_line.setText(default_dir) script_setting = self.get_setting('custom_script') self.script_line.setText(script_setting.value) self.set_window_icon() self.open_export_button.setEnabled(True) self.update_json = True def browse_out_dir(self): self.update_json = False directory = QtGui.QFileDialog.getExistingDirectory(self, "Choose Output Directory", (self.output_line.text() or self.project_dir() or self.last_project_dir)) if directory: self.update_json = True self.output_line.setText(directory) def get_file(self, obj, text_obj, setting, *args, **kwargs): file_path, _ = QtGui.QFileDialog.getOpenFileName(self, 'Choose File', (setting.last_value or self.project_dir() or QtCore.QDir.currentPath()), setting.file_types) if file_path: file_path = os.path.abspath(file_path) # fixes an issue with windows paths file_path = file_path.replace(self.project_dir()+os.path.sep, '') text_obj.setText(file_path) setting.last_value = file_path def get_color(self, obj, color_disp, initial, setting, *args, **kwargs): color = QtGui.QColorDialog.getColor(QtGui.QColor(initial), obj, 'Choose Color') if color: color_disp.setColor(color) setting.last_value = color.name() def get_file_reg(self, obj, text_obj, setting, file_types, *args, **kwargs): file_path, _ = QtGui.QFileDialog.getOpenFileName(self, 'Choose File', (setting.last_value or self.project_dir() or QtCore.QDir.currentPath()), file_types) if file_path: file_path = os.path.abspath(file_path) # fixes an issue with windows paths file_path = file_path.replace(self.project_dir()+os.path.sep, '') text_obj.setText(file_path) setting.last_value = file_path def get_folder(self, obj, text_obj, setting, *args, **kwargs): folder = QtGui.QFileDialog.getExistingDirectory(self, 'Choose Folder', (setting.last_value or QtCore.QDir.currentPath())) if folder: folder = folder.replace(self.project_dir()+os.path.sep, '') text_obj.setText(folder) setting.last_value = folder def create_application_settings(self): group_box = QtGui.QWidget() vlayout = self.create_layout(self.settings['order']['application_setting_order'], cols=3) group_box.setLayout(vlayout) return group_box def create_compression_settings(self): group_box = QtGui.QWidget() vlayout = self.create_layout(self.settings['order']['compression_setting_order'], cols=1) warning_label = QtGui.QLabel('Note: When using compression (greater than 0) it will decrease the executable size,\nbut will increase the startup time when running it.') vbox = QtGui.QVBoxLayout() vbox.addLayout(vlayout) vbox.addWidget(warning_label) group_box.setLayout(vbox) return group_box def create_setting(self, name): setting = self.get_setting(name) if setting.type == 'string': return self.create_text_input_setting(name) elif setting.type == 'strings': return self.create_text_input_setting(name) elif setting.type == 'file': return self.create_text_input_with_file_setting(name) elif setting.type == 'folder': return self.create_text_input_with_folder_setting(name) elif setting.type == 'check': return self.create_check_setting(name) elif setting.type == 'list': return self.create_list_setting(name) elif setting.type == 'range': return self.create_range_setting(name) elif setting.type == 'color': return self.create_color_setting(name) else: print('Setting "{}" type "{}" not defined.'.format(name, setting.type)) return QtGui.QHBoxLayout() def create_window_settings(self): group_box = QtGui.QWidget() vlayout = self.create_layout(self.settings['order']['window_setting_order'], cols=3) group_box.setLayout(vlayout) return group_box def create_web_prefs_settings(self): group_box = QtGui.QWidget() vlayout = self.create_layout(self.settings['order']['web_prefs_order'], cols=3) group_box.setLayout(vlayout) return group_box def create_export_settings(self): group_box = QtGui.QWidget() vlayout = self.create_layout(self.settings['order']['export_setting_order'], cols=4) output_layout = QtGui.QHBoxLayout() output_label = QtGui.QLabel('Output Directory:') output_label.setMinimumWidth(150) self.output_line = QtGui.QLineEdit() self.output_line.textChanged.connect(self.call_with_object('setting_changed', self.output_line, self.get_setting('export_dir'))) self.output_line.textChanged.connect(self.project_path_changed) self.output_line.setStatusTip('The output directory relative to the project directory.') output_button = QtGui.QPushButton('...') output_button.clicked.connect(self.browse_out_dir) output_layout.addWidget(output_label) output_layout.addWidget(self.output_line) output_layout.addWidget(output_button) script_layout = QtGui.QHBoxLayout() script_label = QtGui.QLabel('Execute Script:') script_label.setMinimumWidth(150) self.script_line = QtGui.QLineEdit() script_setting = self.get_setting('custom_script') self.script_line.setObjectName(script_setting.name) self.script_line.textChanged.connect(self.call_with_object('setting_changed', self.script_line, script_setting)) self.script_line.setStatusTip('The script to execute after a project was successfully exported.') script_button = QtGui.QPushButton('...') file_types = ['*.py'] if platform.system() == 'Windows': file_types.append('*.bat') else: file_types.append('*.bash') script_button.clicked.connect(self.call_with_object('get_file_reg', script_button, self.script_line, script_setting, ' '.join(file_types))) script_layout.addWidget(script_label) script_layout.addWidget(self.script_line) script_layout.addWidget(script_button) vbox = QtGui.QVBoxLayout() vbox.addLayout(vlayout) vbox.addLayout(output_layout) vbox.addLayout(script_layout) group_box.setLayout(vbox) return group_box def create_download_settings(self): group_box = QtGui.QWidget() vlayout = self.create_layout(self.settings['order']['download_setting_order'], cols=1) group_box.setLayout(vlayout) return group_box def create_layout(self, settings, cols=3): glayout = QtGui.QGridLayout() glayout.setContentsMargins(10, 15, 10, 5) glayout.setAlignment(QtCore.Qt.AlignTop) glayout.setSpacing(10) glayout.setHorizontalSpacing(20) col = 0 row = 0 for setting_name in settings: setting = self.get_setting(setting_name) if col >= cols*2: row += 1 col = 0 display_name = setting.display_name+':' if setting.required: display_name += '*' setting_label = QtGui.QLabel(display_name) setting_label.setToolTip(setting.description) setting_label.setStatusTip(setting.description) glayout.addWidget(setting_label, row, col) sett = self.create_setting(setting_name) glayout.addLayout(sett, row, col+1) col += 2 return glayout def create_text_input_setting(self, name): hlayout = QtGui.QHBoxLayout() setting = self.get_setting(name) text = QtGui.QLineEdit() text.setValidator(Validator(setting.filter, setting.filter_action)) text.setObjectName(setting.name) text.textChanged.connect(self.call_with_object('setting_changed', text, setting)) if setting.value: text.setText(str(setting.value)) text.setStatusTip(setting.description) text.setToolTip(setting.description) hlayout.addWidget(text) return hlayout def create_text_input_with_file_setting(self, name): hlayout = QtGui.QHBoxLayout() setting = self.get_setting(name) text = QtGui.QLineEdit() text.setObjectName(setting.name) button = QtGui.QPushButton('...') button.setMaximumWidth(30) button.setMaximumHeight(26) button.clicked.connect(self.call_with_object('get_file', button, text, setting)) if setting.value: text.setText(setting.value) text.setStatusTip(setting.description) text.setToolTip(setting.description) text.textChanged.connect(self.call_with_object('setting_changed', text, setting)) hlayout.addWidget(text) hlayout.addWidget(button) return hlayout def create_color_setting(self, name): hlayout = QtGui.QHBoxLayout() setting = self.get_setting(name) color_disp = ColorDisplay(setting.value or setting.default_value) color_disp.setObjectName(setting.name) color_disp.setContentsMargins(5, 5, 5, 5) button = QtGui.QPushButton('Choose...') button.setMaximumWidth(100) button.setMaximumHeight(26) button.clicked.connect(self.call_with_object('get_color', button, color_disp, setting.value or setting.default_value, setting)) if setting.value: color_disp.setColor(setting.value) color_disp.setStatusTip(setting.description) color_disp.setToolTip(setting.description) color_disp.colorChanged.connect(self.call_with_object('setting_changed', color_disp, setting)) hlayout.addWidget(color_disp) hlayout.addWidget(button) return hlayout def create_text_input_with_folder_setting(self, name): hlayout = QtGui.QHBoxLayout() setting = self.get_setting(name) text = QtGui.QLineEdit() text.setObjectName(setting.name) button = QtGui.QPushButton('...') button.setMaximumWidth(30) button.setMaximumHeight(26) button.clicked.connect(self.call_with_object('get_folder', button, text, setting)) if setting.value: text.setText(setting.value) text.setStatusTip(setting.description) text.setToolTip(setting.description) text.textChanged.connect(self.call_with_object('setting_changed', text, setting)) hlayout.addWidget(text) hlayout.addWidget(button) return hlayout def reset_settings(self): for sgroup in self.settings['setting_groups']: for setting in sgroup.values(): widget = self.find_child_by_name(setting.name) if widget is None: continue if (setting.type == 'string' or setting.type == 'file' or setting.type == 'folder'): old_val = '' if setting.default_value is not None: old_val = setting.default_value setting.value = old_val.replace('\\', '\\\\') widget.setText(old_val) elif setting.type == 'strings': old_val = [] if setting.default_value is not None: old_val = setting.default_value setting.value = [v.replace('\\', '\\\\') for v in old_val] widget.setText(','.join(setting.value)) elif setting.type == 'check': old_val = False if setting.default_value is not None: old_val = setting.default_value setting.value = old_val widget.setChecked(old_val) elif setting.type == 'range': old_val = 0 if setting.default_value is not None: old_val = setting.default_value setting.value = old_val widget.setValue(old_val) elif setting.type == 'color': old_val = '' if setting.default_value is not None: old_val = setting.default_value setting.value = old_val widget.setColor(setting.value) def set_kiosk_emulation_options(self, is_checked): if is_checked: width_field = self.find_child_by_name('width') width_field.setText(self.desktop_width) height_field = self.find_child_by_name('height') height_field.setText(self.desktop_height) toolbar_field = self.find_child_by_name('toolbar') toolbar_field.setChecked(not is_checked) frame_field = self.find_child_by_name('frame') frame_field.setChecked(not is_checked) show_field = self.find_child_by_name('show') show_field.setChecked(is_checked) kiosk_field = self.find_child_by_name('kiosk') kiosk_field.setChecked(not is_checked) fullscreen_field = self.find_child_by_name('fullscreen') fullscreen_field.setChecked(not is_checked) always_on_top_field = self.find_child_by_name('always-on-top') always_on_top_field.setChecked(is_checked) resizable_field = self.find_child_by_name('resizable') resizable_field.setChecked(not is_checked) def setting_changed(self, obj, setting, *args, **kwargs): if (setting.type == 'string' or setting.type == 'file' or setting.type == 'folder'): setting.value = setting.convert(args[0]) elif setting.type == 'strings': setting.value = args[0].split(',') elif setting.type == 'check': setting.value = obj.isChecked() check_action = setting.check_action if hasattr(self, check_action): getattr(self, check_action)(obj.isChecked()) elif setting.type == 'list': setting.value = obj.currentText() elif setting.type == 'range': setting.value = obj.value() elif setting.type == 'color': setting.value = args[0].name() if setting.action is not None: action = getattr(self, setting.action, None) if callable(action): action() if self.update_json: json_file = utils.path_join(self.project_dir(), 'package.json') global_json = utils.get_data_file_path('files/global.json') with codecs.open(json_file, 'w+', encoding='utf-8') as f: f.write(self.generate_json()) with codecs.open(global_json, 'w+', encoding='utf-8') as f: f.write(self.generate_json(global_json=True)) self.ex_button.setEnabled(self.required_settings_filled()) def project_path_changed(self, text): self.ex_button.setEnabled(self.required_settings_filled(True)) dirs_filled_out = False if self.project_dir() and self.output_dir(): if os.path.exists(self.project_dir()): dirs_filled_out = True self.option_settings_enabled(dirs_filled_out) def create_check_setting(self, name): hlayout = QtGui.QHBoxLayout() setting = self.get_setting(name) check = QtGui.QCheckBox() check.setObjectName(setting.name) check.clicked.connect(self.call_with_object('setting_changed', check, setting)) check.setChecked(setting.value) check.setStatusTip(setting.description) check.setToolTip(setting.description) hlayout.addWidget(check) return hlayout def create_list_setting(self, name): hlayout = QtGui.QHBoxLayout() setting = self.get_setting(name) button = None if setting.button: button = QtGui.QPushButton(setting.button) button.clicked.connect(lambda: setting.button_callback(button)) combo = QtGui.QComboBox() combo.setObjectName(setting.name) combo.currentIndexChanged.connect(self.call_with_object('setting_changed', combo, setting)) combo.editTextChanged.connect(self.call_with_object('setting_changed', combo, setting)) combo.setStatusTip(setting.description) combo.setToolTip(setting.description) for val in setting.values: combo.addItem(val) default_index = combo.findText(setting.default_value) if default_index != -1: combo.setCurrentIndex(default_index) hlayout.addWidget(QtGui.QLabel()) hlayout.addWidget(combo) if button: hlayout.addWidget(button) return hlayout def create_range_setting(self, name): hlayout = QtGui.QHBoxLayout() setting = self.get_setting(name) button = None if setting.button: button = QtGui.QPushButton(setting.button) button.clicked.connect(lambda: setting.button_callback(button)) slider = QtGui.QSlider(QtCore.Qt.Orientation.Horizontal) slider.setRange(setting.min, setting.max) slider.valueChanged.connect(self.call_with_object('setting_changed', slider, setting)) slider.setMinimumWidth(60) slider.setObjectName(setting.name) slider.setValue(setting.default_value) slider.setStatusTip(setting.description) slider.setToolTip(setting.description) range_label = QtGui.QLabel(str(setting.default_value)) range_label.setMaximumWidth(45) slider.valueChanged.connect(self.call_with_object('_update_range_label', range_label, setting)) w = QtGui.QWidget() whlayout = QtGui.QHBoxLayout() whlayout.addWidget(slider) whlayout.addWidget(range_label) w.setLayout(whlayout) hlayout.addWidget(w) return hlayout def _update_range_label(self, label, setting, value): label.setText(str(value)+setting.label_suffix) def load_package_json(self, json_path=None): setting_list = super(MainWindow, self).load_package_json(json_path) for setting in setting_list: setting_field = self.find_child_by_name(setting.name) if setting_field: if (setting.type == 'file' or setting.type == 'string' or setting.type == 'folder'): val_str = self.convert_val_to_str(setting.value) setting_field.setText(setting.filter_name(val_str)) if setting.type == 'strings': vals = [self.convert_val_to_str(v) for v in setting.value] setting_field.setText(','.join(vals)) if setting.type == 'check': setting_field.setChecked(setting.value) if setting.type == 'list': val_str = self.convert_val_to_str(setting.value) index = setting_field.findText(val_str) if index != -1: setting_field.setCurrentIndex(index) if setting.type == 'range': setting_field.setValue(int(setting.value)) if setting.type == 'color': setting_field.setColor(setting.value) self.ex_button.setEnabled(self.required_settings_filled()) def show_and_raise(self): self.show() self.raise_() self.existing_dialog.show() self.existing_dialog.raise_() if __name__ == '__main__': app = QApplication(sys.argv) QCoreApplication.setApplicationName("Electrify") QCoreApplication.setApplicationVersion(__gui_version__) QCoreApplication.setOrganizationName("SimplyPixelated") QCoreApplication.setOrganizationDomain("simplypixelated.com") frame = MainWindow(1000, 500, app) frame.show_and_raise() sys.exit(app.exec_())