diff --git a/src/maestral_cocoa/bandwidth.py b/src/maestral_cocoa/bandwidth.py new file mode 100644 index 00000000..dcc48276 --- /dev/null +++ b/src/maestral_cocoa/bandwidth.py @@ -0,0 +1,93 @@ +# -*- coding: utf-8 -*- + +from __future__ import annotations + + +# external imports +import toga +from maestral.daemon import MaestralProxy + +# local imports +from .bandwidth_gui import BandwidthGui +from .private.widgets import Window + + +MB_2_BYTES = 10**6 + + +class BandwidthDialog(BandwidthGui): + def __init__(self, mdbx: MaestralProxy, app: toga.App): + super().__init__(app=app, is_dialog=True) + + self.mdbx = mdbx + self.dialog_buttons.on_press = self.on_dialog_pressed + + self.radio_button_unlimited_down.on_change = self.on_limit_downloads_toggled + self.radio_button_limited_down.on_change = self.on_limit_downloads_toggled + self.radio_button_unlimited_up.on_change = self.on_limit_uploads_toggled + self.radio_button_limited_up.on_change = self.on_limit_uploads_toggled + + def refresh_gui(self) -> None: + if self.mdbx.bandwidth_limit_down == 0: + self.radio_button_unlimited_down.value = True + self.number_input_limit_down.enabled = False + else: + self.radio_button_limited_down.value = True + self.number_input_limit_down.value = ( + self.mdbx.bandwidth_limit_down / MB_2_BYTES + ) + self.number_input_limit_down.enabled = True + + if self.mdbx.bandwidth_limit_up == 0: + self.radio_button_unlimited_up.value = True + self.number_input_limit_up.enabled = False + else: + self.radio_button_limited_up.value = True + self.number_input_limit_up.value = self.mdbx.bandwidth_limit_up / MB_2_BYTES + self.number_input_limit_up.enabled = True + + def update_settings(self): + if self.radio_button_unlimited_down.value is True: + self.mdbx.bandwidth_limit_down = 0.0 + else: + self.mdbx.bandwidth_limit_down = ( + float(self.number_input_limit_down.value) * MB_2_BYTES + ) + + if self.radio_button_unlimited_up.value is True: + self.mdbx.bandwidth_limit_up = 0.0 + else: + self.mdbx.bandwidth_limit_up = ( + float(self.number_input_limit_up.value) * MB_2_BYTES + ) + + async def on_limit_downloads_toggled(self, widget: toga.Selection) -> None: + if widget is self.radio_button_unlimited_down: + self.number_input_limit_down.enabled = False + elif widget is self.radio_button_limited_down: + self.number_input_limit_down.enabled = True + else: + raise RuntimeError(f"Unexpected widget {widget}") + + async def on_limit_uploads_toggled(self, widget: toga.Selection) -> None: + if widget is self.radio_button_unlimited_up: + self.number_input_limit_up.enabled = False + elif widget is self.radio_button_limited_up: + self.number_input_limit_up.enabled = True + else: + raise RuntimeError(f"Unexpected widget {widget}") + + async def on_dialog_pressed(self, btn_name: str) -> None: + try: + if btn_name == "Update": + self.update_settings() + finally: + self.close() + + def show_as_sheet(self, window: Window) -> None: + self.refresh_gui() + super().show_as_sheet(window) + + def show(self) -> None: + self.refresh_gui() + super().show() diff --git a/src/maestral_cocoa/bandwidth_gui.py b/src/maestral_cocoa/bandwidth_gui.py new file mode 100644 index 00000000..f4fa76d0 --- /dev/null +++ b/src/maestral_cocoa/bandwidth_gui.py @@ -0,0 +1,129 @@ +# -*- coding: utf-8 -*- + +# external imports +import toga +from toga.style.pack import Pack +from toga.constants import COLUMN, TOP, RIGHT, LEFT + +# local imports +from .private.widgets import Label, RadioButton, Window, DialogButtons + + +class BandwidthGui(Window): + COLUMN_WIDTH_LEFT = 100 + COLUMN_WIDTH_RIGHT = 250 + + ELEMENT_PADDING = 10 + COLUMN_PADDING = 10 + + def __init__(self, **kwargs) -> None: + super().__init__(title="Bandwidth Settings", size=(400, 250), **kwargs) + + self._label_download_rate = Label( + "Download rate:", + style=Pack(text_align=RIGHT, width=BandwidthGui.COLUMN_WIDTH_LEFT), + ) + + self.radio_button_unlimited_down = RadioButton( + "Don't limit", group=RadioButton.Group.A + ) + self.radio_button_limited_down = RadioButton( + "Limit to:", group=RadioButton.Group.A + ) + self.number_input_limit_down = toga.NumberInput( + value=1, + min_value=0.005, + style=Pack(padding_left=BandwidthGui.COLUMN_PADDING, width=50), + ) + self._unit_label_down = toga.Label( + "MB/s", + style=Pack(padding_left=BandwidthGui.COLUMN_PADDING, width=50), + ) + + self._label_upload_rate = Label( + "Upload rate:", + style=Pack(text_align=RIGHT, width=BandwidthGui.COLUMN_WIDTH_LEFT), + ) + + self.radio_button_unlimited_up = RadioButton( + "Don't limit", group=RadioButton.Group.B + ) + self.radio_button_limited_up = RadioButton( + "Limit to:", group=RadioButton.Group.B + ) + self.number_input_limit_up = toga.NumberInput( + value=1, + min_value=0.005, + style=Pack(padding_left=BandwidthGui.COLUMN_PADDING, width=50), + ) + self._unit_label_up = toga.Label( + "MB/s", + style=Pack(padding_left=BandwidthGui.COLUMN_PADDING, width=50), + ) + + self.dialog_buttons = DialogButtons( + labels=["Update", "Cancel"], + style=Pack(padding=(20, 20, 20, 20), flex=1), + ) + + children = [ + toga.Box( + children=[ + self._label_download_rate, + toga.Box( + children=[ + self.radio_button_unlimited_down, + toga.Box( + children=[ + self.radio_button_limited_down, + self.number_input_limit_down, + self._unit_label_down, + ], + ), + ], + style=Pack( + alignment=TOP, + direction=COLUMN, + padding_left=BandwidthGui.COLUMN_PADDING, + ), + ), + ], + ), + toga.Box( + children=[ + self._label_upload_rate, + toga.Box( + children=[ + self.radio_button_unlimited_up, + toga.Box( + children=[ + self.radio_button_limited_up, + self.number_input_limit_up, + self._unit_label_up, + ], + ), + ], + style=Pack( + alignment=TOP, + direction=COLUMN, + padding_left=BandwidthGui.COLUMN_PADDING, + ), + ), + ], + style=Pack(padding_top=BandwidthGui.ELEMENT_PADDING), + ), + self.dialog_buttons, + ] + + main_box = toga.Box( + children=children, + style=Pack( + direction=COLUMN, + padding=30, + width=BandwidthGui.COLUMN_WIDTH_LEFT + BandwidthGui.COLUMN_WIDTH_RIGHT, + flex=1, + alignment=LEFT, + ), + ) + + self.content = main_box diff --git a/src/maestral_cocoa/private/implementation/cocoa/factory.py b/src/maestral_cocoa/private/implementation/cocoa/factory.py index 3822f059..202bb6b8 100644 --- a/src/maestral_cocoa/private/implementation/cocoa/factory.py +++ b/src/maestral_cocoa/private/implementation/cocoa/factory.py @@ -50,6 +50,7 @@ NSURL, NSButton, NSSwitchButton, + NSRadioButton, NSBundle, ) from toga_cocoa.colors import native_color @@ -474,6 +475,43 @@ def rehint(self): ) +class RadioButtonTarget(NSObject): + interface = objc_property(object, weak=True) + impl = objc_property(object, weak=True) + + @objc_method + def onPressA_(self, obj: objc_id) -> None: + if self.interface.on_change: + self.interface.on_change(self.interface) + + @objc_method + def onPressB_(self, obj: objc_id) -> None: + if self.interface.on_change: + self.interface.on_change(self.interface) + + +class RadioButton(Switch): + """Similar to toga_cocoa.Switch but allows *programmatic* setting of + an intermediate state.""" + + def create(self): + self.native = NSButton.alloc().init() + self.native.setButtonType(NSRadioButton) + self.native.autoresizingMask = NSViewMaxYMargin | NSViewMaxYMargin + + self.target = RadioButtonTarget.alloc().init() + self.target.interface = self.interface + self.target.impl = self + + self.native.target = self.target + + # Add the layout constraints + self.add_constraints() + + def set_group(self, group): + self.native.action = SEL(f"onPress{group.name}:") + + # ==== menus and status bar ============================================================ diff --git a/src/maestral_cocoa/private/widgets.py b/src/maestral_cocoa/private/widgets.py index 648f001f..e1e18422 100644 --- a/src/maestral_cocoa/private/widgets.py +++ b/src/maestral_cocoa/private/widgets.py @@ -1,6 +1,8 @@ # -*- coding: utf-8 -*- import os import asyncio +import enum +import os # external imports import click @@ -129,6 +131,33 @@ def state(self, value): self._impl.set_state(value) +class RadioButton(Switch): + class Group(enum.Enum): + A = "A" + B = "B" + + def __init__( + self, + text, + group=Group.A, + id=None, + style=None, + on_change=None, + value=False, + enabled=True, + ): + super().__init__(text, id=id, style=style) + + self._impl = self.factory.RadioButton(interface=self) + self.text = text + self._on_change = None + self.value = value + self.on_change = on_change + + self.enabled = enabled + self._impl.set_group(group) + + class FreestandingIconButton(toga.Widget): """A freestanding button with an icon.""" diff --git a/src/maestral_cocoa/settings.py b/src/maestral_cocoa/settings.py index cc7f1e10..cc63bf7b 100644 --- a/src/maestral_cocoa/settings.py +++ b/src/maestral_cocoa/settings.py @@ -26,6 +26,7 @@ from .private.widgets import FileSelectionButton, Switch, apply_round_clipping from .settings_gui import SettingsGui from .selective_sync import SelectiveSyncDialog +from .bandwidth import BandwidthDialog from .resources import FACEHOLDER_PATH from .autostart import AutoStart from .constants import FROZEN @@ -60,7 +61,9 @@ def __init__(self, mdbx: MaestralProxy, app: MaestralGui) -> None: self.btn_unlink.on_press = self.on_unlink_pressed self.btn_select_folders.on_press = self.on_folder_selection_pressed + self.btn_bandwidth.on_press = self.on_bandwidth_pressed self.combobox_dbx_location.on_select = self.on_dbx_location_selected + self.combobox_update_interval.on_select = self.on_update_interval_selected self.checkbox_autostart.on_change = self.on_autostart_clicked self.checkbox_notifications.on_change = self.on_notifications_clicked @@ -112,6 +115,9 @@ async def on_dbx_location_selected(self, widget: FileSelectionButton) -> None: def on_folder_selection_pressed(self, widget: Any) -> None: SelectiveSyncDialog(mdbx=self.mdbx, app=self.app).show_as_sheet(self) + def on_bandwidth_pressed(self, widget: Any) -> None: + BandwidthDialog(mdbx=self.mdbx, app=self.app).show_as_sheet(self) + async def on_unlink_pressed(self, widget: Any) -> None: should_unlink = await self.confirm_dialog( title="Unlink your Dropbox account?", diff --git a/src/maestral_cocoa/settings_gui.py b/src/maestral_cocoa/settings_gui.py index 5620bbb6..489c927e 100644 --- a/src/maestral_cocoa/settings_gui.py +++ b/src/maestral_cocoa/settings_gui.py @@ -142,16 +142,35 @@ def __init__(self, **kwargs) -> None: ), ) - dropbox_settings_box = toga.Box( + self._label_bandwidth = Label( + "Bandwidth limits:", + style=Pack(text_align=RIGHT, width=SettingsGui.COLUMN_WIDTH_LEFT), + ) + self.btn_bandwidth = toga.Button( + "Change settings...", + style=Pack( + padding_left=SettingsGui.COLUMN_PADDING, width=SettingsGui.BUTTON_WIDTH + ), + ) + + sync_settings_box = toga.Box( children=[ toga.Box( children=[self._label_select_folders, self.btn_select_folders], style=Pack( - alignment=CENTER, padding_bottom=SettingsGui.ELEMENT_PADDING + alignment=CENTER, + padding_bottom=SettingsGui.ELEMENT_PADDING, ), ), toga.Box( children=[self._label_dbx_location, self.combobox_dbx_location], + style=Pack( + alignment=CENTER, + padding_bottom=SettingsGui.ELEMENT_PADDING, + ), + ), + toga.Box( + children=[self._label_bandwidth, self.btn_bandwidth], style=Pack(alignment=CENTER), ), ], @@ -269,7 +288,7 @@ def __init__(self, **kwargs) -> None: ) ) - maestral_settings_box = toga.Box( + system_settings_box = toga.Box( children=children, style=Pack(direction=COLUMN), ) @@ -318,9 +337,9 @@ def __init__(self, **kwargs) -> None: children=[ account_info_box, toga.Divider(style=Pack(padding=SettingsGui.SECTION_PADDING)), - dropbox_settings_box, + sync_settings_box, toga.Divider(style=Pack(padding=SettingsGui.SECTION_PADDING)), - maestral_settings_box, + system_settings_box, toga.Divider(style=Pack(padding=SettingsGui.SECTION_PADDING)), about_box, ],