diff --git a/Makefile b/Makefile index 14b99a1..6cc53b5 100644 --- a/Makefile +++ b/Makefile @@ -47,9 +47,23 @@ bandit: rsync:: bash .rsync.sh -install:: +install:: install-pip + +install-setup:: +# deprecated + ${PYTHON_EXE_MSYS} -W ignore::DeprecationWarning \ + setup.py install \ + --prefix ${PREFIX} + +install-pip:: # we install --nodeps because pip is installing stuff we already have in the OS ${PIP_EXE_MSYS} --python ${PYTHON_EXE_MSYS} install \ - --no-deps \ + --no-deps --no-index \ --target ${PREFIX}/lib/python${PYTHON_MINOR}/site-packages/ \ --upgrade . + +veryclean:: clean + rm -rf build dist $(MOD).egg-info + +clean:: + find . -type f -name \*~ -delete diff --git a/pyproject.toml b/pyproject.toml index 18210c8..197811b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,8 +39,9 @@ repository = "https://git.plastiras.org/emdee/qweechat" homepage = "https://git.plastiras.org/emdee/qweechat" [build-system] -requires = ["setuptools >= 61.0"] +requires = ["setuptools>=40.8.0", "wheel"] build-backend = "setuptools.build_meta" +# backend = "setuptools.build_meta:__legacy__" [tool.setuptools.dynamic] version = {attr = "qweechat.version.VERSION"} diff --git a/qweechat/__main__.py b/qweechat/__main__.py index 73872d4..a808200 100644 --- a/qweechat/__main__.py +++ b/qweechat/__main__.py @@ -1,5 +1,6 @@ import os import sys +import traceback import logging from qtpy import QtWidgets, QtGui, QtCore @@ -16,9 +17,6 @@ def iMain(lArgs=None): except ImportError as e: LOG.error(f"ImportError Loading import qweechat {e} {sys.path}") LOG.debug(traceback.print_exc()) - text = f"ImportError Loading import qweechat {e} {sys.path}" - title = util_ui.tr('Error importing qweechat') - util_ui.message_box(text, title) return 1 from qtpy.QtWidgets import QApplication diff --git a/qweechat/preferences.py b/qweechat/preferences.py index fc415da..0bfd244 100644 --- a/qweechat/preferences.py +++ b/qweechat/preferences.py @@ -22,32 +22,32 @@ """Preferences dialog box.""" -from PyQt5 import QtCore, QtWidgets as QtGui +from qtpy import QtCore, QtWidgets, QtGui +qicon_from_theme = QtGui.QIcon.fromTheme - -class PreferencesDialog(QtGui.QDialog): +class PreferencesDialog(QtWidgets.QDialog): """Preferences dialog.""" def __init__(self, *args): - QtGui.QDialog.__init__(*(self,) + args) + QtWidgets.QDialog.__init__(*(self,) + args) self.setModal(True) self.setWindowTitle('Preferences') - close_button = QtGui.QPushButton('Close') + close_button = QtWidgets.QPushButton('Close') close_button.pressed.connect(self.close) - hbox = QtGui.QHBoxLayout() + hbox = QtWidgets.QHBoxLayout() hbox.addStretch(1) hbox.addWidget(close_button) hbox.addStretch(1) - vbox = QtGui.QVBoxLayout() + vbox = QtWidgets.QVBoxLayout() - label = QtGui.QLabel('Not yet implemented!') + label = QtWidgets.QLabel('Not yet implemented!') label.setAlignment(QtCore.Qt.AlignHCenter) vbox.addWidget(label) - label = QtGui.QLabel('') + label = QtWidgets.QLabel('') label.setAlignment(QtCore.Qt.AlignHCenter) vbox.addWidget(label) @@ -55,3 +55,455 @@ class PreferencesDialog(QtGui.QDialog): self.setLayout(vbox) self.show() + +# prepare for https://github.com/weechat/qweechat/pull/8 + + def _pane_switch(self, item): + """Switch the visible preference pane.""" + index = self.list_panes.indexOfTopLevelItem(item) + if index >= 0: + self.stacked_panes.setCurrentIndex(index) + + def _save_and_close(self): + for widget in (self.stacked_panes.widget(i) + for i in range(self.stacked_panes.count())): + for key, field in widget.fields.items(): + if isinstance(field, QtWidgets.QComboBox): + text = field.itemText(field.currentIndex()) + data = field.itemData(field.currentIndex()) + text = data if data else text + elif isinstance(field, QtWidgets.QCheckBox): + text = "on" if field.isChecked() else "off" + else: + text = field.text() + self.config.set(widget.section_name, key, str(text)) + write(self.config) + self.parent.apply_preferences() + self.close() + + +class PreferencesNotificationBlock(QtWidgets.QVBoxLayout): + """Display notification settings with drill down to configure.""" + def __init__(self, pane, *args): + QtWidgets.QVBoxLayout.__init__(*(self,) + args) + self.section = "notifications" + self.config = QtWidgets.QApplication.instance().config + self.pane = pane + self.stack = QtWidgets.QStackedWidget() + + self.table = QtWidgets.QTableWidget() + fg_color = self.table.palette().text().color().name() + self.action_labels = { + "sound": "Play a sound", + "message": "Show a message in a popup", + "file": "Log to a file", + "taskbar": "Mark taskbar entry", + "tray": "Mark systray/indicator", + "command": "Run a command"} + self.action_icons = { + "sound": qicon_from_theme("media-playback-start"), + "message": qicon_from_theme("dialog-information"), + "file": qicon_from_theme("document-export"), + "taskbar": qicon_from_theme("weechat"), + "tray": utils.qicon_tint("ic_hot", fg_color), + "command": qicon_from_theme("system-run")} + self.icon_widget_qss = "padding:0;min-height:10px;min-width:16px;" + self.table.resizeColumnsToContents() + self.table.setColumnCount(2) + self.table.resizeRowsToContents() + self.table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows) + self.table.setSelectionMode(QtWidgets.QAbstractItemView.SingleSelection) + self.table.setHorizontalHeaderLabels(["State", "Type"]) + self.table.horizontalHeader().setStretchLastSection(True) + self.table.horizontalHeader().setHighlightSections(False) + self.table.verticalHeader().setVisible(False) + self.table.setShowGrid(False) + self.table.itemSelectionChanged.connect(self._table_row_changed) + + self.buftypes = {} + for key, value in CONFIG_DEFAULT_NOTIFICATION_OPTIONS: + buftype, optkey = key.split(".") + if buftype not in self.buftypes: + self.buftypes[buftype] = {} + self.buftypes[buftype][optkey] = self.config.get(self.section, key) + for buftype, optkey in self.buftypes.items(): + self._insert_type(buftype) + self.update_icons() + self.resize_table() + self.addWidget(self.table) + self.addWidget(self.stack) + self.table.selectRow(0) + + def _insert_type(self, buftype): + row = self.table.rowCount() + self.table.insertRow(row) + buftype_item = QtWidgets.QTableWidgetItem(buftype) + buftype_item.setTextAlignment(QtCore.Qt.AlignCenter) + self.table.setItem(row, 0, QtWidgets.QTableWidgetItem()) + self.table.setItem(row, 1, buftype_item) + subgrid = QtWidgets.QGridLayout() + subgrid.setColumnStretch(2, 1) + subgrid.setSpacing(10) + + for key, qicon in self.action_icons.items(): + value = self.buftypes[buftype][key] + line = subgrid.rowCount() + label = IconTextLabel(self.action_labels[key], qicon, 16) + + checkbox = QtWidgets.QCheckBox() + span = 1 + edit = None + if key in ("message", "taskbar", "tray"): + checkbox.setChecked(value == "on") + span = 2 + elif key == "sound": + edit = PreferencesFileEdit( + checkbox=checkbox, caption='Select a sound file', + filter='Audio Files (*.wav *.mp3 *.ogg)') + elif key == "file": + edit = PreferencesFileEdit(checkbox=checkbox, mode="save") + else: + edit = PreferencesFileEdit(checkbox=checkbox) + if edit: + edit.insert(value) + subgrid.addWidget(edit, line, 2) + else: + edit = checkbox + subgrid.addWidget(label, line, 1, 1, span) + subgrid.addWidget(checkbox, line, 0) + self.pane.fields[buftype + "." + key] = edit + subpane = QtWidgets.QWidget() + subpane.setLayout(subgrid) + subpane.setMaximumHeight(subgrid.totalMinimumSize().height()) + self.stack.addWidget(subpane) + + def resize_table(self): + """Fit the table height to contents.""" + height = self.table.horizontalHeader().height() + height = height * (self.table.rowCount() + 1) + height += self.table.contentsMargins().top() + height += self.table.contentsMargins().bottom() + self.table.setMaximumHeight(height) + self.table.setMinimumHeight(height) + self.table.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + + def update_icons(self): + """Draw the correct icons in the left col.""" + for i in range(self.table.rowCount()): + hbox = QtWidgets.QHBoxLayout() + iconset = QtWidgets.QWidget() + buftype = self.table.item(i, 1).text() + for key, qicon in self.action_icons.items(): + field = self.pane.fields[buftype + "." + key] + if isinstance(field, QtWidgets.QCheckBox): + val = "on" if field.isChecked() else "off" + else: + val = field.text() + iconbtn = QtWidgets.QPushButton() + iconbtn.setContentsMargins(0, 0, 0, 0) + iconbtn.setFlat(True) + iconbtn.setFocusPolicy(QtCore.Qt.NoFocus) + if val and val != "off": + iconbtn.setIcon(qicon) + iconbtn.setStyleSheet(self.icon_widget_qss) + iconbtn.setToolTip(key) + iconbtn.clicked.connect(lambda i=i: self.table.selectRow(i)) + hbox.addWidget(iconbtn) + iconset.setLayout(hbox) + self.table.setCellWidget(i, 0, iconset) + + def _table_row_changed(self): + row = self.table.selectionModel().selectedRows()[0].row() + self.stack.setCurrentIndex(row) + + +class PreferencesTreeWidget(QtWidgets.QTreeWidget): + """Widget with tree list of preferences.""" + def __init__(self, header_label, *args): + QtWidgets.QTreeWidget.__init__(*(self,) + args) + self.setHeaderLabel(header_label) + self.setRootIsDecorated(False) + self.setMaximumWidth(180) + self.setTextElideMode(QtCore.Qt.ElideNone) + self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + self.setFocusPolicy(QtCore.Qt.NoFocus) + + +class PreferencesSliderEdit(QtWidgets.QSlider): + """Percentage slider.""" + def __init__(self, *args): + QtWidgets.QSlider.__init__(*(self,) + args) + self.setMinimum(0) + self.setMaximum(100) + self.setTickPosition(QtWidgets.QSlider.TicksBelow) + self.setTickInterval(5) + + def insert(self, percent): + self.setValue(int(percent[:-1])) + + def text(self): + return str(self.value()) + "%" + + +class PreferencesColorEdit(QtWidgets.QPushButton): + """Simple color square that changes based on the color selected.""" + def __init__(self, *args): + QtWidgets.QPushButton.__init__(*(self,) + args) + self.color = "#000000" + self.clicked.connect(self._color_picker) + # Some of the configured colors use a astrisk prefix. + # Toggle this on right click. + self.star = False + self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + self.customContextMenuRequested.connect(self._color_star) + + def insert(self, color): + """Insert the desired color for the widget.""" + if color[:1] == "*": + self.star = True + color = color[1:] + self.setText("*" if self.star else "") + self.color = color + self.setStyleSheet("background-color: " + color) + + def text(self): + """Returns the hex value of the color.""" + return ("*" if self.star else "") + self.color + + def _color_picker(self): + color = QtWidgets.QColorDialog.getColor(self.color) + self.insert(color.name()) + + def _color_star(self): + self.star = not self.star + self.insert(self.text()) + + +class PreferencesFontEdit(QtWidgets.QWidget): + """Font entry and selection.""" + def __init__(self, *args): + QtWidgets.QWidget.__init__(*(self,) + args) + layout = QtWidgets.QHBoxLayout() + self.checkbox = QtWidgets.QCheckBox() + self.edit = QtWidgets.QLineEdit() + self.font = "" + self.qfont = None + self.button = QtWidgets.QPushButton("C&hoose") + self.button.clicked.connect(self._font_picker) + self.checkbox.toggled.connect( + lambda: self._checkbox_toggled(self.checkbox)) + layout.addWidget(self.checkbox) + layout.addWidget(self.edit) + layout.addWidget(self.button) + layout.setContentsMargins(0, 0, 0, 0) + self.setLayout(layout) + + def insert(self, font_str): + """Insert the font described by the string.""" + self.font = font_str + self.edit.insert(font_str) + if font_str: + self.qfont = utils.Font.str_to_qfont(font_str) + self.edit.setFont(self.qfont) + self.checkbox.setChecked(True) + self._checkbox_toggled(self.checkbox) + else: + self.checkbox.setChecked(False) + self.qfont = None + self._checkbox_toggled(self.checkbox) + + def text(self): + """Returns the human readable font string.""" + return self.font + + def _font_picker(self): + font, ok = QtWidgets.QFontDialog.getFont(self.qfont) + if ok: + self.insert(utils.Font.qfont_to_str(font)) + + def _checkbox_toggled(self, button): + if button.isChecked() is False and not self.font == "": + self.insert("") + self.edit.setEnabled(button.isChecked()) + self.button.setEnabled(button.isChecked()) + + +class PreferencesFileEdit(QtWidgets.QWidget): + """File entry and selection.""" + def __init__(self, checkbox=None, caption="Select a file", filter=None, + mode="open", *args): + QtWidgets.QWidget.__init__(*(self,) + args) + layout = QtWidgets.QHBoxLayout() + self.caption = caption + self.filter = filter + self.edit = QtWidgets.QLineEdit() + self.file_str = "" + self.mode = mode + self.button = QtWidgets.QPushButton("B&rowse") + self.button.clicked.connect(self._file_picker) + if checkbox: + self.checkbox = checkbox + else: + self.checkbox = QtWidgets.QCheckBox() + layout.addWidget(self.checkbox) + self.checkbox.toggled.connect( + lambda: self._checkbox_toggled(self.checkbox)) + layout.addWidget(self.edit) + layout.addWidget(self.button) + layout.setContentsMargins(0, 0, 0, 0) + self.setLayout(layout) + + def insert(self, file_str): + """Insert the file.""" + self.file_str = file_str + self.edit.insert(file_str) + if file_str: + self.checkbox.setChecked(True) + self._checkbox_toggled(self.checkbox) + else: + self.checkbox.setChecked(False) + self._checkbox_toggled(self.checkbox) + + def text(self): + """Returns the human readable font string.""" + return self.file_str + + def _file_picker(self): + path = "" + if self.mode == "save": + fn = QtWidgets.QFileDialog.getSaveFileName + else: + fn = QtWidgets.QFileDialog.getOpenFileName + filename, fil = fn(self, self.caption, path, self.filter, self.filter) + if filename: + self.insert(filename) + + def _checkbox_toggled(self, button): + if button.isChecked() is False and not self.file_str == "": + self.insert("") + self.edit.setEnabled(button.isChecked()) + self.button.setEnabled(button.isChecked()) + + +class PreferencesPaneWidget(QtWidgets.QWidget): + """ + Widget with (from top to bottom): + title, chat + nicklist (optional) + prompt/input. + """ + + disabled_fields = ["show_hostnames", "hide_nick_changes", + "hide_join_and_part"] + + def __init__(self, section, section_name): + QtWidgets.QWidget.__init__(self) + self.grid = QtWidgets.QGridLayout() + self.grid.setAlignment(QtCore.Qt.AlignTop) + self.section = section + self.section_name = section_name + self.fields = {} + self.setLayout(self.grid) + self.grid.setColumnStretch(2, 1) + self.grid.setSpacing(10) + self.int_validator = QtWidgets.QIntValidator(0, 2147483647, self) + toolbar_icons = [ + ('ToolButtonFollowStyle', 'Default'), + ('ToolButtonIconOnly', 'Icon Only'), + ('ToolButtonTextOnly', 'Text Only'), + ('ToolButtonTextBesideIcon', 'Text Alongside Icons'), + ('ToolButtonTextUnderIcon', 'Text Under Icons')] + tray_options = [ + ('always', 'Always'), + ('unread', 'On Unread Messages'), + ('never', 'Never'), + ] + list_positions = [ + ('left', 'Left'), + ('right', 'Right'), + ] + sort_options = ['A-Z Ranked', 'A-Z', 'Z-A Ranked', 'Z-A'] + focus_opts = ["requested", "always", "never"] + self.comboboxes = {"style": QtWidgets.QStyleFactory.keys(), + "position": list_positions, + "toolbar_icons": toolbar_icons, + "focus_new_tabs": focus_opts, + "tray_icon": tray_options, + "sort": sort_options} + + def addItem(self, key, value, default): + """Add a key-value pair.""" + line = len(self.fields) + name = key.split(".")[-1:][0].capitalize().replace("_", " ") + label = QtWidgets.QLabel(name) + start = 0 + + if self.section == "color": + start = 2 * (line % 2) + line = line // 2 + edit = PreferencesColorEdit() + edit.setFixedWidth(edit.sizeHint().height()) + edit.insert(value) + label.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) + elif key == "custom_stylesheet": + edit = PreferencesFileEdit(caption='Select QStyleSheet File', + filter='*.qss') + edit.insert(value) + elif name.lower()[-5:] == "sound": + edit = PreferencesFileEdit( + caption='Select a sound file', + filter='Audio Files (*.wav *.mp3 *.ogg)') + edit.insert(value) + elif name.lower()[-4:] == "font": + edit = PreferencesFontEdit() + edit.setFixedWidth(200) + edit.insert(value) + elif key in self.comboboxes.keys(): + edit = QtWidgets.QComboBox() + if len(self.comboboxes[key][0]) == 2: + for keyvalue in self.comboboxes[key]: + edit.addItem(keyvalue[1], keyvalue[0]) + # if self.section == "nicks" and key == "position": + # edit.addItem("below", "Below Buffer List") + # edit.addItem("above", "Above Buffer List") + edit.setCurrentIndex(edit.findData(value)) + else: + edit.addItems(self.comboboxes[key]) + edit.setCurrentIndex(edit.findText(value)) + edit.setFixedWidth(200) + elif default in ["on", "off"]: + edit = QtWidgets.QCheckBox() + edit.setChecked(value == "on") + elif default[-1:] == "%": + edit = PreferencesSliderEdit(QtCore.Qt.Horizontal) + edit.setFixedWidth(200) + edit.insert(value) + else: + edit = QtWidgets.QLineEdit() + edit.setFixedWidth(200) + edit.insert(value) + if default.isdigit() or key == "port": + edit.setValidator(self.int_validator) + if key == 'password': + edit.setEchoMode(QtWidgets.QLineEdit.Password) + if key in self.disabled_fields: + edit.setDisabled(True) + self.grid.addWidget(label, line, start + 0) + self.grid.addWidget(edit, line, start + 1) + + self.fields[key] = edit + + +class IconTextLabel(QtWidgets.QWidget): + """An icon next to text.""" + def __init__(self, text=None, icon=None, extent=None): + QtWidgets.QWidget.__init__(self) + text_label = QtWidgets.QLabel(text) + if not extent: + extent = text_label.height() + icon_label = QtWidgets.QLabel() + pixmap = icon.pixmap(extent, QtWidgets.QIcon.Normal, QtWidgets.QIcon.On) + icon_label.setPixmap(pixmap) + label_layout = QtWidgets.QHBoxLayout() + label_layout.addWidget(icon_label) + label_layout.addWidget(text_label) + label_layout.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter) + self.setLayout(label_layout) diff --git a/qweechat/qweechat.py b/qweechat/qweechat.py index e270362..3dbd14e 100644 --- a/qweechat/qweechat.py +++ b/qweechat/qweechat.py @@ -35,6 +35,7 @@ It requires requires WeeChat 0.3.7 or newer, running on local or remote host. import sys import traceback +import signal from pkg_resources import resource_filename from qtpy import QtCore, QtGui, QtWidgets @@ -541,16 +542,27 @@ class MainWindow(QtWidgets.QMainWindow): write(self.config) QtWidgets.QFrame.closeEvent(self, event) - -def main(): - app = QtWidgets.QApplication(sys.argv) +def iMain(lArgs=None): + if lArgs is None and len(sys.argv) > 1: + lArgs = sys.argv[1:] + elif lArgs is None: + lArgs = [] + app = QtWidgets.QApplication(lArgs) app.setStyle(QtWidgets.QStyleFactory.create('Cleanlooks')) app.setWindowIcon(QtGui.QIcon( resource_filename(__name__, 'data/icons/weechat.png'))) main_win = MainWindow() main_win.show() - sys.exit(app.exec_()) - + i = app.exec_() + return i if __name__ == '__main__': - main() + signal.signal(signal.SIGINT, signal.SIG_DFL) + try: + i = iMain() + except KeyboardInterrupt as e: + i = 0 + except Exception as e: + LOG.exception(f"Exception {e}") + i = 1 + sys.exit(i) diff --git a/qweechat/version.py b/qweechat/version.py index f607118..4997a9c 100644 --- a/qweechat/version.py +++ b/qweechat/version.py @@ -22,7 +22,7 @@ """Version of QWeeChat.""" -VERSION = '0.0.1' +VERSION = '1.0.0' def qweechat_version(): diff --git a/requirements.txt b/requirements.txt index cbd920a..dd3e743 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ # works with PyQt5 and PyQt6 and maybe PySide2 and PySide6 as well PyQt5 # earlier versions may work just fine -dev-python/qtconsole >= 5.5.1 +qtconsole >= 5.5.1 diff --git a/setup.py b/setup.py index 9224aca..8c51dd3 100644 --- a/setup.py +++ b/setup.py @@ -44,12 +44,12 @@ setup( 'Programming Language :: Python', 'Topic :: Communications :: Chat', ], - packages=['qweechat', 'qweechat.weechat'], + packages=['qweechat', 'qweechat.weechat', 'qweechat.data.icons'], include_package_data=True, package_data={'qweechat': ['data/icons/*.png']}, entry_points={ 'gui_scripts': [ - 'qweechat = qweechat.qweechat:main', + 'qweechat = qweechat.qweechat:iMain', ], 'console_scripts': [ 'qweechat-testproto = qweechat.weechat.testproto:main', @@ -57,6 +57,7 @@ setup( }, install_requires=[ 'PyQt5', + 'qtconsole', ], zip_safe = False, )