From 60c48bcb6a1353b7ec29bcebcc583986bb63aba8 Mon Sep 17 00:00:00 2001 From: emdee Date: Sun, 11 Feb 2024 07:50:55 +0000 Subject: [PATCH] new --- Makefile | 17 +- pyproject.toml | 51 ++++ qweechat/buffer.py | 2 +- qweechat/chat.py | 4 +- qweechat/config.py | 1 + qweechat/network.py | 2 +- qweechat/preferences.py | 537 ++-------------------------------- qweechat/preferences.py.new | 555 ++++++++++++++++++++++++++++++++++++ qweechat/qweechat.py | 6 +- qweechat/version.py | 2 +- requirements.txt | 4 +- setup.py | 3 +- 12 files changed, 652 insertions(+), 532 deletions(-) create mode 100644 pyproject.toml create mode 100644 qweechat/preferences.py.new diff --git a/Makefile b/Makefile index 600e2ca..6fb6ec7 100644 --- a/Makefile +++ b/Makefile @@ -16,12 +16,22 @@ # You should have received a copy of the GNU General Public License # along with QWeeChat. If not, see . # +# to run the tests, run make PASS=controllerpassword test + +PREFIX=/usr/local +PYTHON_EXE_MSYS=${PREFIX}/bin/python3.sh +PIP_EXE_MSYS=${PREFIX}/bin/pip3.sh +LOCAL_DOCTEST=${PREFIX}/bin/toxcore_run_doctest3.bash +DOCTEST=${LOCAL_DOCTEST} +PYTHON_MINOR=`python3 --version 2>&1 | sed -e 's@^.* @@' -e 's@\.[0-9]*$$@@'` +MOD=qweechat all: check check: lint test + sh ${PYTHON_EXE_MSYS} -c "import ${MOD}" -lint: flake8 pylint bandit +lint:: flake8 pylint bandit flake8: flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics @@ -32,3 +42,8 @@ pylint: bandit: bandit -r qweechat + +install:: +# we install --nodeps because pip is installing stuff we already have in the OS + ${PYTHON_EXE_MSYS} setup.py install \ + --prefix ${PREFIX}/ diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..dd1a710 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,51 @@ +[project] +name = 'qweechat' +requires-python = ">= 3.7" +description = "qtpy channel for qweechat" +keywords = ["qt", "console"] +classifiers = [ + # How mature is this project? Common values are + # 3 - Alpha + # 4 - Beta + # 5 - Production/Stable + "Development Status :: 4 - Beta", + + # Indicate who your project is intended for + "Intended Audience :: Developers", + + # Specify the Python versions you support here. + "Programming Language :: Python :: 3", + "License :: OSI Approved", + "Operating System :: POSIX :: BSD :: FreeBSD", + "Operating System :: POSIX :: Linux", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: Implementation :: CPython", +] +dynamic = ["version", "readme", "dependencies"] # cannot be dynamic ['license'] + +[project.scripts] +qweechat = "qweechat:iMain" + +[project.license] +file = "COPYING" + +[project.urls] +repository = "https://git.plastiras.org/emdee/qweechat" +homepage = "https://git.plastiras.org/emdee/qweechat" + +[build-system] +requires = ["setuptools >= 61.0"] +build-backend = "setuptools.build_meta" + +[tool.setuptools.dynamic] +version = {attr = "qweechat.version.VERSION"} +readme = {file = ["README.md"]} +dependencies = {file = ["requirements.txt"]} + +#[tool.setuptools.packages.find] +#where = "src" diff --git a/qweechat/buffer.py b/qweechat/buffer.py index 008ecde..9cca40e 100644 --- a/qweechat/buffer.py +++ b/qweechat/buffer.py @@ -25,7 +25,7 @@ from pkg_resources import resource_filename from qtpy import QtCore, QtGui, QtWidgets -from qtpy.QtCore import pyqtSignal as Signal +from qtpy.QtCore import Signal from qweechat.chat import ChatTextEdit from qweechat.input import InputLineEdit diff --git a/qweechat/chat.py b/qweechat/chat.py index 43c4568..6c06aa9 100644 --- a/qweechat/chat.py +++ b/qweechat/chat.py @@ -26,7 +26,7 @@ import datetime from qtpy import QtCore, QtWidgets, QtGui -from qweechat import config +from qweechat.config import color_options from qweechat.weechat import color @@ -62,7 +62,7 @@ class ChatTextEdit(QtWidgets.QTextEdit): '/': True } } - self._color = color.Color(config.color_options(), self.debug) + self._color = color.Color(color_options(), self.debug) def display(self, time, prefix, text, forcecolor=None): if time == 0: diff --git a/qweechat/config.py b/qweechat/config.py index ffdf793..f28450f 100644 --- a/qweechat/config.py +++ b/qweechat/config.py @@ -41,6 +41,7 @@ CONFIG_DEFAULT_OPTIONS = (('relay.hostname', '127.0.0.1'), ('relay.lines', str(CONFIG_DEFAULT_RELAY_LINES)), ('look.debug', 'off'), ('look.statusbar', 'on')) +CONFIG_DEFAULT_NOTIFICATION_OPTIONS = tuple # Default colors for WeeChat color options (option name, #rgb value) CONFIG_DEFAULT_COLOR_OPTIONS = ( diff --git a/qweechat/network.py b/qweechat/network.py index 0127a98..be7246d 100644 --- a/qweechat/network.py +++ b/qweechat/network.py @@ -232,7 +232,7 @@ class Network(QtCore.QObject): def is_connected(self): """Return True if the socket is connected, False otherwise.""" - return is_state(self, at='ConnectedState') + return self.is_state(at='ConnectedState') def is_state(self, at='ConnectedState'): """Return True if the socket is connected, False otherwise.""" diff --git a/qweechat/preferences.py b/qweechat/preferences.py index 97feece..fc415da 100644 --- a/qweechat/preferences.py +++ b/qweechat/preferences.py @@ -2,7 +2,7 @@ # # preferences.py - preferences dialog box # -# Copyright (C) 2016 Ricky Brent +# Copyright (C) 2011-2022 Sébastien Helleu # # This file is part of QWeeChat, a Qt remote GUI for WeeChat. # @@ -20,541 +20,38 @@ # along with QWeeChat. If not, see . # -import qt_compat -import config -import utils -from inputlinespell import InputLineSpell +"""Preferences dialog box.""" -QtCore = qt_compat.import_module('QtCore') -QtGui = qt_compat.import_module('QtGui') +from PyQt5 import QtCore, QtWidgets as QtGui class PreferencesDialog(QtGui.QDialog): """Preferences dialog.""" - custom_sections = { - "look": "Look", - "input": "Input Box", - "nicks": "Nick List", - "buffers": "Buffer List", - "buffer_flags": False, - "notifications": "Notifications", - "color": "Colors", - "relay": "Relay/Connection" - } - - def __init__(self, name, parent, *args): + def __init__(self, *args): QtGui.QDialog.__init__(*(self,) + args) self.setModal(True) - self.setWindowTitle(name) - self.parent = parent - self.config = parent.config - self.stacked_panes = QtGui.QStackedWidget() - self.list_panes = PreferencesTreeWidget("Settings") + self.setWindowTitle('Preferences') - splitter = QtGui.QSplitter() - splitter.addWidget(self.list_panes) - splitter.addWidget(self.stacked_panes) - - # Follow same order as defaults: - section_panes = {} - for section in config.CONFIG_DEFAULT_SECTIONS: - item = QtGui.QTreeWidgetItem(section) - name = section - item.setText(0, section.title()) - if section in self.custom_sections: - if not self.custom_sections[section]: - continue - item.setText(0, self.custom_sections[section]) - section_panes[section] = PreferencesPaneWidget(section, name) - self.list_panes.addTopLevelItem(item) - self.stacked_panes.addWidget(section_panes[section]) - - for setting, default in config.CONFIG_DEFAULT_OPTIONS: - section_key = setting.split(".") - section = section_key[0] - key = ".".join(section_key[1:]) - section_panes[section].addItem( - key, self.config.get(section, key), default) - for key, value in self.config.items("color"): - section_panes["color"].addItem(key, value, False) - notification_field_count = len(section_panes["notifications"].fields) - notification = PreferencesNotificationBlock( - section_panes["notifications"]) - section_panes["notifications"].grid.addLayout( - notification, notification_field_count, 0, 1, -1) - - self.list_panes.currentItemChanged.connect(self._pane_switch) - self.list_panes.setCurrentItem(self.list_panes.topLevelItem(0)) + close_button = QtGui.QPushButton('Close') + close_button.pressed.connect(self.close) hbox = QtGui.QHBoxLayout() - self.dialog_buttons = QtGui.QDialogButtonBox() - self.dialog_buttons.setStandardButtons( - QtGui.QDialogButtonBox.Save | QtGui.QDialogButtonBox.Cancel) - self.dialog_buttons.rejected.connect(self.close) - self.dialog_buttons.accepted.connect(self._save_and_close) - hbox.addStretch(1) - hbox.addWidget(self.dialog_buttons) + hbox.addWidget(close_button) hbox.addStretch(1) vbox = QtGui.QVBoxLayout() - vbox.addWidget(splitter) + + label = QtGui.QLabel('Not yet implemented!') + label.setAlignment(QtCore.Qt.AlignHCenter) + vbox.addWidget(label) + + label = QtGui.QLabel('') + label.setAlignment(QtCore.Qt.AlignHCenter) + vbox.addWidget(label) + vbox.addLayout(hbox) self.setLayout(vbox) self.show() - - 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, QtGui.QComboBox): - text = field.itemText(field.currentIndex()) - data = field.itemData(field.currentIndex()) - text = data if data else text - elif isinstance(field, QtGui.QCheckBox): - text = "on" if field.isChecked() else "off" - else: - text = field.text() - self.config.set(widget.section_name, key, str(text)) - config.write(self.config) - self.parent.apply_preferences() - self.close() - - -class PreferencesNotificationBlock(QtGui.QVBoxLayout): - """Display notification settings with drill down to configure.""" - def __init__(self, pane, *args): - QtGui.QVBoxLayout.__init__(*(self,) + args) - self.section = "notifications" - self.config = QtGui.QApplication.instance().config - self.pane = pane - self.stack = QtGui.QStackedWidget() - - self.table = QtGui.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": utils.qicon_from_theme("media-playback-start"), - "message": utils.qicon_from_theme("dialog-information"), - "file": utils.qicon_from_theme("document-export"), - "taskbar": utils.qicon_from_theme("weechat"), - "tray": utils.qicon_tint("ic_hot", fg_color), - "command": utils.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(QtGui.QAbstractItemView.SelectRows) - self.table.setSelectionMode(QtGui.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.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 = QtGui.QTableWidgetItem(buftype) - buftype_item.setTextAlignment(QtCore.Qt.AlignCenter) - self.table.setItem(row, 0, QtGui.QTableWidgetItem()) - self.table.setItem(row, 1, buftype_item) - subgrid = QtGui.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 = QtGui.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 = QtGui.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 = QtGui.QHBoxLayout() - iconset = QtGui.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, QtGui.QCheckBox): - val = "on" if field.isChecked() else "off" - else: - val = field.text() - iconbtn = QtGui.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(QtGui.QTreeWidget): - """Widget with tree list of preferences.""" - def __init__(self, header_label, *args): - QtGui.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(QtGui.QSlider): - """Percentage slider.""" - def __init__(self, *args): - QtGui.QSlider.__init__(*(self,) + args) - self.setMinimum(0) - self.setMaximum(100) - self.setTickPosition(QtGui.QSlider.TicksBelow) - self.setTickInterval(5) - - def insert(self, percent): - self.setValue(int(percent[:-1])) - - def text(self): - return str(self.value()) + "%" - - -class PreferencesColorEdit(QtGui.QPushButton): - """Simple color square that changes based on the color selected.""" - def __init__(self, *args): - QtGui.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 = QtGui.QColorDialog.getColor(self.color) - self.insert(color.name()) - - def _color_star(self): - self.star = not self.star - self.insert(self.text()) - - -class PreferencesFontEdit(QtGui.QWidget): - """Font entry and selection.""" - def __init__(self, *args): - QtGui.QWidget.__init__(*(self,) + args) - layout = QtGui.QHBoxLayout() - self.checkbox = QtGui.QCheckBox() - self.edit = QtGui.QLineEdit() - self.font = "" - self.qfont = None - self.button = QtGui.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 = QtGui.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(QtGui.QWidget): - """File entry and selection.""" - def __init__(self, checkbox=None, caption="Select a file", filter=None, - mode="open", *args): - QtGui.QWidget.__init__(*(self,) + args) - layout = QtGui.QHBoxLayout() - self.caption = caption - self.filter = filter - self.edit = QtGui.QLineEdit() - self.file_str = "" - self.mode = mode - self.button = QtGui.QPushButton("B&rowse") - self.button.clicked.connect(self._file_picker) - if checkbox: - self.checkbox = checkbox - else: - self.checkbox = QtGui.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 = QtGui.QFileDialog.getSaveFileName - else: - fn = QtGui.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(QtGui.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): - QtGui.QWidget.__init__(self) - self.grid = QtGui.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 = QtGui.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'] - spellcheck_langs = [(x, x) for x in - InputLineSpell.list_languages()] - spellcheck_langs.insert(0, ('', '')) - focus_opts = ["requested", "always", "never"] - self.comboboxes = {"style": QtGui.QStyleFactory.keys(), - "position": list_positions, - "toolbar_icons": toolbar_icons, - "focus_new_tabs": focus_opts, - "tray_icon": tray_options, - "sort": sort_options, - "spellcheck_dictionary": spellcheck_langs} - - def addItem(self, key, value, default): - """Add a key-value pair.""" - line = len(self.fields) - name = key.split(".")[-1:][0].capitalize().replace("_", " ") - label = QtGui.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 = QtGui.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 = QtGui.QCheckBox() - edit.setChecked(value == "on") - elif default[-1:] == "%": - edit = PreferencesSliderEdit(QtCore.Qt.Horizontal) - edit.setFixedWidth(200) - edit.insert(value) - else: - edit = QtGui.QLineEdit() - edit.setFixedWidth(200) - edit.insert(value) - if default.isdigit() or key == "port": - edit.setValidator(self.int_validator) - if key == 'password': - edit.setEchoMode(QtGui.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(QtGui.QWidget): - """An icon next to text.""" - def __init__(self, text=None, icon=None, extent=None): - QtGui.QWidget.__init__(self) - text_label = QtGui.QLabel(text) - if not extent: - extent = text_label.height() - icon_label = QtGui.QLabel() - pixmap = icon.pixmap(extent, QtGui.QIcon.Normal, QtGui.QIcon.On) - icon_label.setPixmap(pixmap) - label_layout = QtGui.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/preferences.py.new b/qweechat/preferences.py.new new file mode 100644 index 0000000..8103b27 --- /dev/null +++ b/qweechat/preferences.py.new @@ -0,0 +1,555 @@ +# -*- coding: utf-8 -*- +# +# preferences.py - preferences dialog box +# +# Copyright (C) 2016 Ricky Brent +# +# This file is part of QWeeChat, a Qt remote GUI for WeeChat. +# +# QWeeChat is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# QWeeChat is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with QWeeChat. If not, see . +# + +from qweechat.config import (CONFIG_DEFAULT_SECTIONS, + CONFIG_DEFAULT_NOTIFICATION_OPTIONS, + CONFIG_DEFAULT_OPTIONS, write) +import utils + +from qtpy import QtCore, QtGui +qicon_from_theme = QtGui.QIcon.fromTheme + +class PreferencesDialog(QtGui.QDialog): + """Preferences dialog.""" + + custom_sections = { + "look": "Look", + "input": "Input Box", + "nicks": "Nick List", + "buffers": "Buffer List", + "buffer_flags": False, + "notifications": "Notifications", + "color": "Colors", + "relay": "Relay/Connection" + } + + def __init__(self, name, parent, *args): + QtGui.QDialog.__init__(*(self,) + args) + self.setModal(True) + self.setWindowTitle(name) + self.parent = parent + self.config = parent.config + self.stacked_panes = QtGui.QStackedWidget() + self.list_panes = PreferencesTreeWidget("Settings") + + splitter = QtGui.QSplitter() + splitter.addWidget(self.list_panes) + splitter.addWidget(self.stacked_panes) + + # Follow same order as defaults: + section_panes = {} + for section in CONFIG_DEFAULT_SECTIONS: + item = QtGui.QTreeWidgetItem(section) + name = section + item.setText(0, section.title()) + if section in self.custom_sections: + if not self.custom_sections[section]: + continue + item.setText(0, self.custom_sections[section]) + section_panes[section] = PreferencesPaneWidget(section, name) + self.list_panes.addTopLevelItem(item) + self.stacked_panes.addWidget(section_panes[section]) + + for setting, default in CONFIG_DEFAULT_OPTIONS: + section_key = setting.split(".") + section = section_key[0] + key = ".".join(section_key[1:]) + section_panes[section].addItem( + key, self.config.get(section, key), default) + for key, value in self.config.items("color"): + section_panes["color"].addItem(key, value, False) + notification_field_count = len(section_panes["notifications"].fields) + notification = PreferencesNotificationBlock( + section_panes["notifications"]) + section_panes["notifications"].grid.addLayout( + notification, notification_field_count, 0, 1, -1) + + self.list_panes.currentItemChanged.connect(self._pane_switch) + self.list_panes.setCurrentItem(self.list_panes.topLevelItem(0)) + + hbox = QtGui.QHBoxLayout() + self.dialog_buttons = QtGui.QDialogButtonBox() + self.dialog_buttons.setStandardButtons( + QtGui.QDialogButtonBox.Save | QtGui.QDialogButtonBox.Cancel) + self.dialog_buttons.rejected.connect(self.close) + self.dialog_buttons.accepted.connect(self._save_and_close) + + hbox.addStretch(1) + hbox.addWidget(self.dialog_buttons) + hbox.addStretch(1) + + vbox = QtGui.QVBoxLayout() + vbox.addWidget(splitter) + vbox.addLayout(hbox) + + self.setLayout(vbox) + self.show() + + 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, QtGui.QComboBox): + text = field.itemText(field.currentIndex()) + data = field.itemData(field.currentIndex()) + text = data if data else text + elif isinstance(field, QtGui.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(QtGui.QVBoxLayout): + """Display notification settings with drill down to configure.""" + def __init__(self, pane, *args): + QtGui.QVBoxLayout.__init__(*(self,) + args) + self.section = "notifications" + self.config = QtGui.QApplication.instance().config + self.pane = pane + self.stack = QtGui.QStackedWidget() + + self.table = QtGui.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(QtGui.QAbstractItemView.SelectRows) + self.table.setSelectionMode(QtGui.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 = QtGui.QTableWidgetItem(buftype) + buftype_item.setTextAlignment(QtCore.Qt.AlignCenter) + self.table.setItem(row, 0, QtGui.QTableWidgetItem()) + self.table.setItem(row, 1, buftype_item) + subgrid = QtGui.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 = QtGui.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 = QtGui.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 = QtGui.QHBoxLayout() + iconset = QtGui.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, QtGui.QCheckBox): + val = "on" if field.isChecked() else "off" + else: + val = field.text() + iconbtn = QtGui.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(QtGui.QTreeWidget): + """Widget with tree list of preferences.""" + def __init__(self, header_label, *args): + QtGui.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(QtGui.QSlider): + """Percentage slider.""" + def __init__(self, *args): + QtGui.QSlider.__init__(*(self,) + args) + self.setMinimum(0) + self.setMaximum(100) + self.setTickPosition(QtGui.QSlider.TicksBelow) + self.setTickInterval(5) + + def insert(self, percent): + self.setValue(int(percent[:-1])) + + def text(self): + return str(self.value()) + "%" + + +class PreferencesColorEdit(QtGui.QPushButton): + """Simple color square that changes based on the color selected.""" + def __init__(self, *args): + QtGui.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 = QtGui.QColorDialog.getColor(self.color) + self.insert(color.name()) + + def _color_star(self): + self.star = not self.star + self.insert(self.text()) + + +class PreferencesFontEdit(QtGui.QWidget): + """Font entry and selection.""" + def __init__(self, *args): + QtGui.QWidget.__init__(*(self,) + args) + layout = QtGui.QHBoxLayout() + self.checkbox = QtGui.QCheckBox() + self.edit = QtGui.QLineEdit() + self.font = "" + self.qfont = None + self.button = QtGui.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 = QtGui.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(QtGui.QWidget): + """File entry and selection.""" + def __init__(self, checkbox=None, caption="Select a file", filter=None, + mode="open", *args): + QtGui.QWidget.__init__(*(self,) + args) + layout = QtGui.QHBoxLayout() + self.caption = caption + self.filter = filter + self.edit = QtGui.QLineEdit() + self.file_str = "" + self.mode = mode + self.button = QtGui.QPushButton("B&rowse") + self.button.clicked.connect(self._file_picker) + if checkbox: + self.checkbox = checkbox + else: + self.checkbox = QtGui.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 = QtGui.QFileDialog.getSaveFileName + else: + fn = QtGui.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(QtGui.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): + QtGui.QWidget.__init__(self) + self.grid = QtGui.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 = QtGui.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": QtGui.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 = QtGui.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 = QtGui.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 = QtGui.QCheckBox() + edit.setChecked(value == "on") + elif default[-1:] == "%": + edit = PreferencesSliderEdit(QtCore.Qt.Horizontal) + edit.setFixedWidth(200) + edit.insert(value) + else: + edit = QtGui.QLineEdit() + edit.setFixedWidth(200) + edit.insert(value) + if default.isdigit() or key == "port": + edit.setValidator(self.int_validator) + if key == 'password': + edit.setEchoMode(QtGui.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(QtGui.QWidget): + """An icon next to text.""" + def __init__(self, text=None, icon=None, extent=None): + QtGui.QWidget.__init__(self) + text_label = QtGui.QLabel(text) + if not extent: + extent = text_label.height() + icon_label = QtGui.QLabel() + pixmap = icon.pixmap(extent, QtGui.QIcon.Normal, QtGui.QIcon.On) + icon_label.setPixmap(pixmap) + label_layout = QtGui.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 ed6187a..49d7a68 100644 --- a/qweechat/qweechat.py +++ b/qweechat/qweechat.py @@ -39,7 +39,7 @@ from pkg_resources import resource_filename from qtpy import QtCore, QtGui, QtWidgets -from qweechat import config +from qweechat.config import read, write from qweechat.about import AboutDialog from qweechat.buffer import BufferListWidget, Buffer from qweechat.connection import ConnectionDialog @@ -59,7 +59,7 @@ class MainWindow(QtWidgets.QMainWindow): def __init__(self, *args): super().__init__(*args) - self.config = config.read() + self.config = read() self.resize(1000, 600) self.setWindowTitle(APP_NAME) @@ -536,7 +536,7 @@ class MainWindow(QtWidgets.QMainWindow): self.network.disconnect_weechat() if self.network.debug_dialog: self.network.debug_dialog.close() - config.write(self.config) + write(self.config) QtWidgets.QFrame.closeEvent(self, event) diff --git a/qweechat/version.py b/qweechat/version.py index 25bbf46..f607118 100644 --- a/qweechat/version.py +++ b/qweechat/version.py @@ -22,7 +22,7 @@ """Version of QWeeChat.""" -VERSION = '0.0.1-dev' +VERSION = '0.0.1' def qweechat_version(): diff --git a/requirements.txt b/requirements.txt index df9b5f9..122c03a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ # works with PyQt5 and maybe PySide as well -PyQt6 - +# PyQt5 +# dev-python/qtconsole >= 5.5.1 diff --git a/setup.py b/setup.py index dfdbe06..9224aca 100644 --- a/setup.py +++ b/setup.py @@ -56,6 +56,7 @@ setup( ] }, install_requires=[ - 'PyQt6', + 'PyQt5', ], + zip_safe = False, )