2019-09-10 00:54:28 +00:00
|
|
|
# Created By: Virgil Dupras
|
|
|
|
# Created On: 2011-02-01
|
|
|
|
# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
|
|
|
|
#
|
|
|
|
# This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
|
|
|
|
# which should be included with this package. The terms are also available at
|
|
|
|
# http://www.gnu.org/licenses/gpl-3.0.html
|
|
|
|
|
|
|
|
import sys
|
|
|
|
import io
|
|
|
|
import os.path as op
|
|
|
|
import os
|
|
|
|
import logging
|
2022-07-07 03:40:36 +00:00
|
|
|
from typing import List, Union
|
2019-09-10 00:54:28 +00:00
|
|
|
|
2021-08-18 02:04:09 +00:00
|
|
|
from core.util import executable_folder
|
2019-09-10 00:54:28 +00:00
|
|
|
from hscommon.util import first
|
2022-05-09 01:33:31 +00:00
|
|
|
from hscommon.plat import ISWINDOWS
|
2019-09-10 00:54:28 +00:00
|
|
|
|
2022-07-07 03:40:36 +00:00
|
|
|
from PyQt6.QtCore import QStandardPaths, QSettings
|
|
|
|
from PyQt6.QtGui import QPixmap, QIcon, QGuiApplication, QAction
|
|
|
|
from PyQt6.QtWidgets import QSpacerItem, QSizePolicy, QHBoxLayout, QWidget
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
|
2022-07-07 03:40:36 +00:00
|
|
|
def move_to_screen_center(widget: QWidget) -> None:
|
2019-09-10 00:54:28 +00:00
|
|
|
frame = widget.frameGeometry()
|
2021-08-28 04:26:19 +00:00
|
|
|
if QGuiApplication.screenAt(frame.center()) is None:
|
|
|
|
# if center not on any screen use default screen
|
|
|
|
screen = QGuiApplication.screens()[0].availableGeometry()
|
|
|
|
else:
|
|
|
|
screen = QGuiApplication.screenAt(frame.center()).availableGeometry()
|
|
|
|
# moves to center of screen if partially off screen
|
|
|
|
if screen.contains(frame) is False:
|
|
|
|
# make sure the frame is not larger than screen
|
|
|
|
# resize does not seem to take frame size into account (move does)
|
|
|
|
widget.resize(frame.size().boundedTo(screen.size() - (frame.size() - widget.size())))
|
|
|
|
frame = widget.frameGeometry()
|
|
|
|
frame.moveCenter(screen.center())
|
|
|
|
widget.move(frame.topLeft())
|
2019-09-10 00:54:28 +00:00
|
|
|
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2022-07-07 03:40:36 +00:00
|
|
|
def vertical_spacer(size: Union[int, None] = None) -> QSpacerItem:
|
2019-09-10 00:54:28 +00:00
|
|
|
if size:
|
2022-07-07 03:40:36 +00:00
|
|
|
return QSpacerItem(1, size, QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)
|
2019-09-10 00:54:28 +00:00
|
|
|
else:
|
2022-07-07 03:40:36 +00:00
|
|
|
return QSpacerItem(1, 1, QSizePolicy.Policy.Fixed, QSizePolicy.Policy.MinimumExpanding)
|
2019-09-10 00:54:28 +00:00
|
|
|
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2022-07-07 03:40:36 +00:00
|
|
|
def horizontal_spacer(size: Union[int, None] = None) -> QSpacerItem:
|
2019-09-10 00:54:28 +00:00
|
|
|
if size:
|
2022-07-07 03:40:36 +00:00
|
|
|
return QSpacerItem(size, 1, QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)
|
2019-09-10 00:54:28 +00:00
|
|
|
else:
|
2022-07-07 03:40:36 +00:00
|
|
|
return QSpacerItem(1, 1, QSizePolicy.Policy.MinimumExpanding, QSizePolicy.Policy.Fixed)
|
2019-09-10 00:54:28 +00:00
|
|
|
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2022-07-07 03:40:36 +00:00
|
|
|
def horizontal_wrap(widgets: List[Union[QWidget, int, None]]) -> QHBoxLayout:
|
2019-09-10 00:54:28 +00:00
|
|
|
"""Wrap all widgets in `widgets` in a horizontal layout.
|
|
|
|
|
|
|
|
If, instead of placing a widget in your list, you place an int or None, an horizontal spacer
|
|
|
|
with the width corresponding to the int will be placed (0 or None means an expanding spacer).
|
|
|
|
"""
|
|
|
|
layout = QHBoxLayout()
|
|
|
|
for widget in widgets:
|
|
|
|
if widget is None or isinstance(widget, int):
|
2021-08-24 05:12:23 +00:00
|
|
|
layout.addItem(horizontal_spacer(size=widget))
|
2019-09-10 00:54:28 +00:00
|
|
|
else:
|
|
|
|
layout.addWidget(widget)
|
|
|
|
return layout
|
|
|
|
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2021-08-24 05:12:23 +00:00
|
|
|
def create_actions(actions, target):
|
|
|
|
# actions are list of (name, shortcut, icon, desc, func)
|
2019-09-10 00:54:28 +00:00
|
|
|
for name, shortcut, icon, desc, func in actions:
|
|
|
|
action = QAction(target)
|
|
|
|
if icon:
|
2022-07-07 03:40:36 +00:00
|
|
|
action.setIcon(QIcon(QPixmap(":/" + icon))) # TODO stop using qrc file path
|
2019-09-10 00:54:28 +00:00
|
|
|
if shortcut:
|
|
|
|
action.setShortcut(shortcut)
|
|
|
|
action.setText(desc)
|
|
|
|
action.triggered.connect(func)
|
|
|
|
setattr(target, name, action)
|
|
|
|
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2021-08-24 05:12:23 +00:00
|
|
|
def set_accel_keys(menu):
|
2019-09-10 00:54:28 +00:00
|
|
|
actions = menu.actions()
|
|
|
|
titles = [a.text() for a in actions]
|
|
|
|
available_characters = {c.lower() for s in titles for c in s if c.isalpha()}
|
|
|
|
for action in actions:
|
|
|
|
text = action.text()
|
|
|
|
c = first(c for c in text if c.lower() in available_characters)
|
|
|
|
if c is None:
|
|
|
|
continue
|
|
|
|
i = text.index(c)
|
2020-01-01 02:16:27 +00:00
|
|
|
newtext = text[:i] + "&" + text[i:]
|
2019-09-10 00:54:28 +00:00
|
|
|
available_characters.remove(c.lower())
|
|
|
|
action.setText(newtext)
|
|
|
|
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2022-07-07 03:40:36 +00:00
|
|
|
def get_appdata(portable: bool = False) -> str:
|
2021-08-18 02:04:09 +00:00
|
|
|
if portable:
|
|
|
|
return op.join(executable_folder(), "data")
|
|
|
|
else:
|
2022-07-07 03:40:36 +00:00
|
|
|
return QStandardPaths.standardLocations(QStandardPaths.StandardLocation.AppDataLocation)[0]
|
2019-09-10 00:54:28 +00:00
|
|
|
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
class SysWrapper(io.IOBase):
|
|
|
|
def write(self, s):
|
2020-01-01 02:16:27 +00:00
|
|
|
if s.strip(): # don't log empty stuff
|
2019-09-10 00:54:28 +00:00
|
|
|
logging.warning(s)
|
|
|
|
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2021-08-24 05:12:23 +00:00
|
|
|
def setup_qt_logging(level=logging.WARNING, log_to_stdout=False):
|
2019-09-10 00:54:28 +00:00
|
|
|
# Under Qt, we log in "debug.log" in appdata. Moreover, when under cx_freeze, we have a
|
|
|
|
# problem because sys.stdout and sys.stderr are None, so we need to replace them with a
|
|
|
|
# wrapper that logs with the logging module.
|
2021-08-24 05:12:23 +00:00
|
|
|
appdata = get_appdata()
|
2019-09-10 00:54:28 +00:00
|
|
|
if not op.exists(appdata):
|
|
|
|
os.makedirs(appdata)
|
2020-01-01 02:16:27 +00:00
|
|
|
# Setup logging
|
2019-09-10 00:54:28 +00:00
|
|
|
# Have to use full configuration over basicConfig as FileHandler encoding was not being set.
|
2020-01-01 02:16:27 +00:00
|
|
|
filename = op.join(appdata, "debug.log") if not log_to_stdout else None
|
2019-09-10 00:54:28 +00:00
|
|
|
log = logging.getLogger()
|
2020-01-01 02:16:27 +00:00
|
|
|
handler = logging.FileHandler(filename, "a", "utf-8")
|
|
|
|
formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")
|
2019-09-10 00:54:28 +00:00
|
|
|
handler.setFormatter(formatter)
|
|
|
|
log.addHandler(handler)
|
2020-01-01 02:16:27 +00:00
|
|
|
if sys.stderr is None: # happens under a cx_freeze environment
|
2019-09-10 00:54:28 +00:00
|
|
|
sys.stderr = SysWrapper()
|
|
|
|
if sys.stdout is None:
|
|
|
|
sys.stdout = SysWrapper()
|
|
|
|
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2021-08-24 05:12:23 +00:00
|
|
|
def escape_amp(s):
|
2019-09-10 00:54:28 +00:00
|
|
|
# Returns `s` with escaped ampersand (& --> &&). QAction text needs to have & escaped because
|
|
|
|
# that character is used to define "accel keys".
|
2020-01-01 02:16:27 +00:00
|
|
|
return s.replace("&", "&&")
|
2022-05-09 01:33:31 +00:00
|
|
|
|
|
|
|
|
2022-07-07 03:40:36 +00:00
|
|
|
def create_qsettings() -> QSettings:
|
2022-05-09 01:33:31 +00:00
|
|
|
# Create a QSettings instance with the correct arguments.
|
|
|
|
config_location = op.join(executable_folder(), "settings.ini")
|
|
|
|
if op.isfile(config_location):
|
2022-07-07 03:40:36 +00:00
|
|
|
settings = QSettings(config_location, QSettings.Format.IniFormat)
|
2022-05-09 01:33:31 +00:00
|
|
|
settings.setValue("Portable", True)
|
|
|
|
elif ISWINDOWS:
|
|
|
|
# On windows use an ini file in the AppDataLocation instead of registry if possible as it
|
|
|
|
# makes it easier for a user to clear it out when there are issues.
|
2022-07-07 03:40:36 +00:00
|
|
|
locations = QStandardPaths.standardLocations(QStandardPaths.StandardLocation.AppDataLocation)
|
2022-05-09 01:33:31 +00:00
|
|
|
if locations:
|
2022-07-07 03:40:36 +00:00
|
|
|
settings = QSettings(op.join(locations[0], "settings.ini"), QSettings.Format.IniFormat)
|
2022-05-09 01:33:31 +00:00
|
|
|
else:
|
|
|
|
settings = QSettings()
|
|
|
|
settings.setValue("Portable", False)
|
|
|
|
else:
|
|
|
|
settings = QSettings()
|
|
|
|
settings.setValue("Portable", False)
|
|
|
|
return settings
|