mirror of
https://github.com/Codeux-Software/Textual.git
synced 2026-06-16 13:24:32 +00:00
3799 lines
91 KiB
Objective-C
Executable File
3799 lines
91 KiB
Objective-C
Executable File
/* *********************************************************************
|
|
* _____ _ _
|
|
* |_ _|____ _| |_ _ _ __ _| |
|
|
* | |/ _ \ \/ / __| | | |/ _` | |
|
|
* | | __/> <| |_| |_| | (_| | |
|
|
* |_|\___/_/\_\\__|\__,_|\__,_|_|
|
|
*
|
|
* Copyright (c) 2008 - 2010 Satoshi Nakagawa <psychs AT limechat DOT net>
|
|
* Copyright (c) 2010 - 2020 Codeux Software, LLC & respective contributors.
|
|
* Please see Acknowledgements.pdf for additional information.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
*
|
|
* * Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* * Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
* * Neither the name of Textual, "Codeux Software, LLC", nor the
|
|
* names of its contributors may be used to endorse or promote products
|
|
* derived from this software without specific prior written permission.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
|
|
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
* ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
|
|
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
|
|
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
|
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
|
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
|
|
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
|
|
* SUCH DAMAGE.
|
|
*
|
|
*********************************************************************** */
|
|
|
|
#import "NSObjectHelperPrivate.h"
|
|
#import "IRCClientConfig.h"
|
|
#import "IRCClientPrivate.h"
|
|
#import "IRCChannelPrivate.h"
|
|
#import "IRCChannelMode.h"
|
|
#import "IRCChannelUser.h"
|
|
#import "IRCExtrasPrivate.h"
|
|
#import "IRCISupportInfo.h"
|
|
#import "IRCUser.h"
|
|
#import "IRCWorldPrivate.h"
|
|
#import "TVCBasicTableView.h"
|
|
#import "TVCLogController.h"
|
|
#import "TVCLogViewPrivate.h"
|
|
#import "TVCLogViewInternalWK2.h"
|
|
#import "TVCMemberList.h"
|
|
#import "TVCMainWindowPrivate.h"
|
|
#import "TVCMainWindowSplitView.h"
|
|
#import "TVCMainWindowTextView.h"
|
|
#import "TLOEncryptionManagerPrivate.h"
|
|
#import "TLOLicenseManagerPrivate.h"
|
|
#import "TLOLocalization.h"
|
|
#import "TLOpenLink.h"
|
|
#import "TDCAboutDialogPrivate.h"
|
|
#import "TDCAlert.h"
|
|
#import "TDCChannelInviteSheetPrivate.h"
|
|
#import "TDCChannelModifyModesSheetPrivate.h"
|
|
#import "TDCChannelModifyTopicSheetPrivate.h"
|
|
#import "TDCChannelPropertiesSheetPrivate.h"
|
|
#import "TDCChannelSpotlightControllerPrivate.h"
|
|
#import "TDCFileTransferDialogPrivate.h"
|
|
#import "TDCInputPrompt.h"
|
|
#import "TDCLicenseManagerDialogPrivate.h"
|
|
#import "TDCNicknameColorSheetPrivate.h"
|
|
#import "TDCPreferencesControllerPrivate.h"
|
|
#import "TDCServerChangeNicknameSheetPrivate.h"
|
|
#import "TDCServerHighlightListSheetPrivate.h"
|
|
#import "TDCServerPropertiesSheetPrivate.h"
|
|
#import "TDCWelcomeSheetPrivate.h"
|
|
#import "TPCPathInfoPrivate.h"
|
|
#import "TPCPreferencesImportExport.h"
|
|
#import "TPCPreferencesLocalPrivate.h"
|
|
#import "TPCPreferencesReload.h"
|
|
#import "TPCPreferencesUserDefaults.h"
|
|
#import "TXMasterControllerPrivate.h"
|
|
#import "TXWindowControllerPrivate.h"
|
|
#import "TXMenuControllerPrivate.h"
|
|
|
|
#if TEXTUAL_BUILT_WITH_SPARKLE_ENABLED == 1
|
|
#import <Sparkle/Sparkle.h>
|
|
#endif
|
|
|
|
NS_ASSUME_NONNULL_BEGIN
|
|
|
|
#define _popWindowViewIfExists(c) if ([windowController() maybeBringWindowForward:(c)]) { \
|
|
return; \
|
|
}
|
|
|
|
@interface TXMenuController () <NSMenuItemValidation>
|
|
@property (nonatomic, assign) BOOL menuIsOpen;
|
|
@property (nonatomic, assign) BOOL menuPerformedActionLastOpen;
|
|
@property (nonatomic, weak) IRCClient *pointedClient;
|
|
@property (nonatomic, weak) IRCChannel *pointedChannel;
|
|
@property (nonatomic, copy) NSString *currentSearchPhrase;
|
|
@property (readonly, nullable) TVCLogController *selectedViewController;
|
|
@property (readonly, nullable) TVCLogView *selectedViewControllerBackingView;
|
|
@property (readonly) TDCFileTransferDialog *fileTransferController;
|
|
@property (nonatomic, strong, readwrite) IBOutlet NSMenu *channelViewChannelNameMenu;
|
|
@property (nonatomic, strong, readwrite) IBOutlet NSMenu *channelViewGeneralMenu;
|
|
@property (nonatomic, strong, readwrite) IBOutlet NSMenu *channelViewURLMenu;
|
|
@property (nonatomic, strong, readwrite) IBOutlet NSMenu *dockMenu;
|
|
|
|
#if TEXTUAL_BUILT_WITH_ADVANCED_ENCRYPTION == 1
|
|
@property (nonatomic, strong, readwrite) IBOutlet NSMenu *encryptionManagerStatusMenu;
|
|
#endif
|
|
|
|
@property (nonatomic, weak, readwrite) IBOutlet NSMenu *mainMenuNavigationChannelListMenu;
|
|
@property (nonatomic, weak, readwrite) IBOutlet NSMenu *mainMenuChannelMenu;
|
|
@property (nonatomic, weak, readwrite) IBOutlet NSMenu *mainMenuQueryMenu;
|
|
@property (nonatomic, weak, readwrite) IBOutlet NSMenuItem *mainMenuChannelMenuItem;
|
|
@property (nonatomic, weak, readwrite) IBOutlet NSMenuItem *mainMenuQueryMenuItem;
|
|
@property (nonatomic, weak, readwrite) IBOutlet NSMenuItem *mainMenuServerMenuItem;
|
|
@property (nonatomic, weak, readwrite) IBOutlet NSMenuItem *mainMenuWindowMenuItem;
|
|
@property (nonatomic, strong, readwrite) IBOutlet NSMenu *mainWindowSegmentedControllerCellMenu;
|
|
@property (nonatomic, strong, readwrite) IBOutlet NSMenu *serverListNoSelectionMenu;
|
|
@property (nonatomic, strong, readwrite) IBOutlet NSMenu *userControlMenu;
|
|
@property (nonatomic, weak, readwrite) IBOutlet NSMenuItem *muteNotificationsDockMenuItem;
|
|
@property (nonatomic, weak, readwrite) IBOutlet NSMenuItem *muteNotificationsFileMenuItem;
|
|
@property (nonatomic, weak, readwrite) IBOutlet NSMenuItem *muteNotificationsSoundsDockMenuItem;
|
|
@property (nonatomic, weak, readwrite) IBOutlet NSMenuItem *muteNotificationsSoundsFileMenuItem;
|
|
@end
|
|
|
|
@implementation TXMenuController
|
|
|
|
- (void)prepareInitialState
|
|
{
|
|
self.currentSearchPhrase = @"";
|
|
|
|
if ([TPCPreferences soundIsMuted]) {
|
|
self.muteNotificationsSoundsDockMenuItem.state = NSControlStateValueOn;
|
|
self.muteNotificationsSoundsFileMenuItem.state = NSControlStateValueOn;
|
|
}
|
|
|
|
[self.channelViewGeneralMenu itemWithTag:MTWKGeneralChannelMenu].submenu = [self.mainMenuChannelMenu copy];
|
|
|
|
[self setupOtherServices];
|
|
|
|
[RZNotificationCenter() addObserver:self selector:@selector(menuItemWillPerformedAction:) name:NSMenuWillSendActionNotification object:nil];
|
|
[RZNotificationCenter() addObserver:self selector:@selector(menuItemPerformedAction:) name:NSMenuDidSendActionNotification object:nil];
|
|
|
|
[RZNotificationCenter() addObserver:self selector:@selector(mainWindowSelectionChanged:) name:TVCMainWindowSelectionChangedNotification object:nil];
|
|
}
|
|
|
|
- (void)setupOtherServices
|
|
{
|
|
[self.fileTransferController startUsingDownloadDestinationURL];
|
|
}
|
|
|
|
- (void)prepareForApplicationTermination
|
|
{
|
|
LogToConsoleTerminationProgress("Preparing menu controller");
|
|
|
|
[self.fileTransferController prepareForApplicationTermination];
|
|
}
|
|
|
|
- (void)preferencesChanged
|
|
{
|
|
[self.fileTransferController clearIPAddress];
|
|
}
|
|
|
|
- (void)mainWindowSelectionChanged:(NSNotification *)notification
|
|
{
|
|
if (self.menuIsOpen == NO) {
|
|
[self resetSelectedItems];
|
|
}
|
|
|
|
/* When the selection changes, menus that may be dynamic are force
|
|
revalidated so that Command I (or other shortcuts) work with channel
|
|
selected, but not for the server console. */
|
|
NSMenuItem *channelMenu = self.mainMenuChannelMenuItem;
|
|
|
|
[self _forceMenuItemValidation:channelMenu];
|
|
|
|
NSMenuItem *queryMenu = self.mainMenuQueryMenuItem;
|
|
|
|
[self _forceMenuItemValidation:queryMenu];
|
|
}
|
|
|
|
- (void)_forceMenuValidation:(NSMenu *)menu
|
|
{
|
|
NSParameterAssert(menu != nil);
|
|
|
|
for (NSMenuItem *menuItem in menu.itemArray) {
|
|
[self _forceMenuItemValidation:menuItem];
|
|
}
|
|
}
|
|
|
|
- (void)_forceMenuItemValidation:(NSMenuItem *)menuItem
|
|
{
|
|
NSParameterAssert(menuItem != nil);
|
|
|
|
id target = menuItem.target;
|
|
|
|
if (target == nil) {
|
|
return;
|
|
}
|
|
|
|
if ([target respondsToSelector:@selector(validateMenuItem:)]) {
|
|
[target performSelector:@selector(validateMenuItem:) withObject:menuItem];
|
|
}
|
|
}
|
|
|
|
- (BOOL)validateMenuItem:(NSMenuItem *)menuItem
|
|
{
|
|
NSParameterAssert(menuItem != nil);
|
|
|
|
if (masterController().applicationIsTerminating) {
|
|
return NO;
|
|
}
|
|
|
|
/* Menu validation works in two passes:
|
|
1. First -_validateMenuItem: is called which performs validation for
|
|
the individual menu item including hiding it and related items.
|
|
2. The result is then passed to the logic below which performs more
|
|
specialized work such as disabling large group of menu items when
|
|
the trial has expired.
|
|
*/
|
|
BOOL validationResult = [self _validateMenuItem:menuItem];
|
|
|
|
if (validationResult == NO) {
|
|
return NO;
|
|
}
|
|
|
|
NSUInteger tag = menuItem.tag;
|
|
|
|
/* The submenus of the main menu are all targets of the menu
|
|
controller so that we can chose to hide or show some depending
|
|
on context. For the top most submenus, we have nothing further
|
|
to do after performing initial validation. */
|
|
switch (tag) {
|
|
case MTMainMenuApp:
|
|
case MTMainMenuFile:
|
|
case MTMainMenuEdit:
|
|
case MTMainMenuView:
|
|
case MTMainMenuServer:
|
|
case MTMainMenuChannel:
|
|
case MTMainMenuQuery:
|
|
case MTMainMenuNavigate:
|
|
case MTMainMenuWindow:
|
|
case MTMainMenuHelp:
|
|
{
|
|
return YES;
|
|
}
|
|
} // switch
|
|
|
|
/* When the main window is not the focused window or when we are
|
|
in a sheet, most items can be disabled which means at this point
|
|
we will default to disabled and allow the bottom logic to enable
|
|
only the bare essentials. */
|
|
BOOL defaultToNoForSheet = ( mainWindow().attachedSheet != nil ||
|
|
(mainWindow().mainWindow == NO &&
|
|
mainWindow().isBeneathMouse == NO));
|
|
|
|
if (defaultToNoForSheet) {
|
|
validationResult = NO;
|
|
}
|
|
|
|
/* If the app has not finished launching,
|
|
then default everything to disabled. */
|
|
if (masterController().applicationIsLaunched == NO) {
|
|
validationResult = NO;
|
|
}
|
|
|
|
/* If trial is expired, then default everything to disabled. */
|
|
BOOL isTrialExpired = NO;
|
|
|
|
#if TEXTUAL_BUILT_WITH_LICENSE_MANAGER == 1
|
|
if (TLOLicenseManagerTextualIsRegistered() == NO && TLOLicenseManagerIsTrialExpired()) {
|
|
/* Set flag letting logic know trial expired */
|
|
isTrialExpired = YES;
|
|
|
|
/* Disable everything by default except "Manage license…" */
|
|
validationResult = (tag == MTMMAppManageLicense);
|
|
} // if
|
|
#endif
|
|
|
|
/* If certain items are hidden because of sheet but not because
|
|
of the trial being expired, then enable additional items. */
|
|
if (validationResult == NO && defaultToNoForSheet && isTrialExpired == NO) {
|
|
switch (tag) {
|
|
case MTMMAppAboutApp: // "About Textual"
|
|
case MTMMAppPreferences: // "Preferences…"
|
|
case MTMMAppManageLicense: // "Manage license…"
|
|
case MTMMAppCheckForUpdates: // "Check for updates…"
|
|
case MTMMHelpAdvancedMenuEnableDeveloperMode: // "Enable Developer Mode"
|
|
case MTMMHelpAdvancedMenuHiddenPreferences: // "Hidden Preferences…"
|
|
case MTMMFileDisableAllNotifications: // "Disable All Notifications"
|
|
case MTMMFileDisableAllNotificationSounds: // "Disable All Notification Sounds"
|
|
case MTDockMenuDisableAllNotifications: // "Disable All Notifications"
|
|
case MTDockMenuDisableAllNotificationSounds: // "Disable All Notification Sounds"
|
|
{
|
|
validationResult = YES;
|
|
|
|
break;
|
|
}
|
|
} // switch
|
|
} // if
|
|
|
|
/* These are the bare minimum of menu items that must be enabled
|
|
at all times because they are essential to the entire application. */
|
|
/* This list may look incomplete but it isn't. Many menu items,
|
|
such as Undo, Cut, Copy, Quit, etc. are not a target of the menu
|
|
controller which means they never pass through this logic. */
|
|
if (validationResult == NO) {
|
|
switch (tag) {
|
|
case MTMMAppAboutApp: // "About Textual"
|
|
case MTMMAppQuitApp: // "Quit Textual & IRC"
|
|
case MTMMFilePrint: // "Print"
|
|
case MTMMFileCloseWindow: // "Close Window"
|
|
case MTMMEditPaste: // "Paste"
|
|
case MTMMViewToggleFullscreen: // "Toggle Fullscreen"
|
|
case MTMMWindowMainWindow: // "Main Window"
|
|
case MTMMHelpAcknowledgements: // "Acknowledgements"
|
|
case MTMMHelpLicenseAgreement: // "License Agreement"
|
|
case MTMMHelpPrivacyPolicy: // "Privacy Policy"
|
|
case MTMMHelpFrequentlyAskedQuestions: // "Frequently Asked Questions"
|
|
case MTMMHelpKnowledgeBaseMenu: // "Knowledge Base"
|
|
case MTMMHelpAdvancedMenu: // "Advanced"
|
|
case MTMMHelpAdvancedMenuExportPreferences: // "Export Preferences"
|
|
{
|
|
validationResult = YES;
|
|
|
|
break;
|
|
}
|
|
default:
|
|
{
|
|
if (menuItem.parentItem.tag == MTMMHelpKnowledgeBaseMenu) {
|
|
validationResult = YES;
|
|
}
|
|
|
|
break;
|
|
}
|
|
} // switch
|
|
} // if
|
|
|
|
return validationResult;
|
|
}
|
|
|
|
- (BOOL)_validateMenuItem:(NSMenuItem *)menuItem
|
|
{
|
|
NSParameterAssert(menuItem != nil);
|
|
|
|
NSUInteger tag = menuItem.tag;
|
|
|
|
IRCClient *u = mainWindow().selectedClient;
|
|
IRCChannel *c = mainWindow().selectedChannel;
|
|
|
|
switch (tag) {
|
|
case MTMainMenuChannel: // "Channel"
|
|
{
|
|
BOOL isChannel = c.isChannel;
|
|
|
|
menuItem.hidden = (isChannel == NO);
|
|
|
|
if (isChannel) {
|
|
menuItem.submenu = self.mainMenuChannelMenu;
|
|
} else {
|
|
menuItem.submenu = nil;
|
|
}
|
|
|
|
return YES;
|
|
}
|
|
case MTMainMenuQuery: // "Query"
|
|
{
|
|
BOOL isQuery = (c.isPrivateMessage || c.isUtility);
|
|
|
|
menuItem.hidden = (isQuery == NO);
|
|
|
|
if (isQuery) {
|
|
menuItem.submenu = self.mainMenuQueryMenu;
|
|
} else {
|
|
menuItem.submenu = nil;
|
|
}
|
|
|
|
return YES;
|
|
}
|
|
|
|
case MTMMAppManageLicense: // "Manage license…"
|
|
{
|
|
#if TEXTUAL_BUILT_WITH_LICENSE_MANAGER == 0
|
|
menuItem.hidden = YES;
|
|
#endif
|
|
|
|
return YES;
|
|
}
|
|
case MTMMAppCheckForUpdates: // "Check for Updates"
|
|
{
|
|
#if TEXTUAL_BUILT_WITH_SPARKLE_ENABLED == 0
|
|
menuItem.hidden = YES;
|
|
#endif
|
|
|
|
return YES;
|
|
}
|
|
|
|
case MTMMFileCloseWindow: // "Close Window"
|
|
{
|
|
TXCommandWKeyAction keyAction = [TPCPreferences commandWKeyAction];
|
|
|
|
if (keyAction == TXCommandWKeyActionCloseWindow || mainWindow().keyWindow == NO) {
|
|
menuItem.title = TXTLS(@"BasicLanguage[1f6-bg]");
|
|
|
|
return YES;
|
|
}
|
|
|
|
if (u == nil) {
|
|
return NO;
|
|
}
|
|
|
|
switch (keyAction) {
|
|
case TXCommandWKeyActionPartChannel:
|
|
{
|
|
if (c == nil) {
|
|
menuItem.title = TXTLS(@"BasicLanguage[1f6-bg]");
|
|
|
|
return NO;
|
|
}
|
|
|
|
if (c.isChannel) {
|
|
menuItem.title = TXTLS(@"BasicLanguage[5td-3f]");
|
|
|
|
if (c.isActive == NO) {
|
|
return NO;
|
|
}
|
|
} else if (c.isPrivateMessage) {
|
|
menuItem.title = TXTLS(@"BasicLanguage[hri-l0]");
|
|
} else if (c.isUtility) {
|
|
menuItem.title = TXTLS(@"BasicLanguage[hri-l0]");
|
|
}
|
|
|
|
break;
|
|
}
|
|
case TXCommandWKeyActionDisconnect:
|
|
{
|
|
menuItem.title = TXTLS(@"BasicLanguage[w3a-je]", u.networkNameAlt);
|
|
|
|
if (u.isConnecting == NO && u.isConnected == NO) {
|
|
return NO;
|
|
}
|
|
|
|
break;
|
|
}
|
|
case TXCommandWKeyActionTerminate:
|
|
{
|
|
menuItem.title = TXTLS(@"BasicLanguage[x97-ro]");
|
|
|
|
break;
|
|
}
|
|
default:
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
return YES;
|
|
}
|
|
|
|
case MTMMEditPaste: // "Paste"
|
|
case MTWKGeneralPaste: // "Paste" (WebView)
|
|
{
|
|
NSString *currentPasteboard = RZPasteboard().stringContent;
|
|
|
|
if (currentPasteboard.length == 0) {
|
|
return NO;
|
|
}
|
|
|
|
if (mainWindow().keyWindow) {
|
|
return mainWindowTextField().editable;
|
|
}
|
|
|
|
id firstResponder = [NSApp keyWindow].firstResponder;
|
|
|
|
if ([firstResponder respondsToSelector:@selector(isEditable)]) {
|
|
return [firstResponder isEditable];
|
|
}
|
|
|
|
return NO;
|
|
}
|
|
|
|
case MTMMViewMarkScrollback: // "Mark Scrollback"
|
|
case MTMMViewScrollbackMarker: // "Scrollback Marker"
|
|
case MTMMViewMarkAllAsRead: // "Mark All as Read"
|
|
case MTMMViewClearScrollback: // "Clear Scrollback"
|
|
case MTMMViewIncreaseFontSize: // "Increase Font Size"
|
|
case MTMMViewDecreaseFontSize: // "Decrease Font Size"
|
|
case MTMMNavigationJumpToCurrentSession: // "Jump to Current Session"
|
|
case MTMMNavigationJumpToPresent: // "Jump to Present"
|
|
{
|
|
return (self.selectedViewController != nil);
|
|
}
|
|
case MTMMViewToggleFullscreen:
|
|
{
|
|
NSWindowCollectionBehavior collectionBehavior = [NSApp keyWindow].collectionBehavior;
|
|
|
|
return ((collectionBehavior & NSWindowCollectionBehaviorFullScreenAuxiliary) == NSWindowCollectionBehaviorFullScreenAuxiliary ||
|
|
(collectionBehavior & NSWindowCollectionBehaviorFullScreenPrimary) == NSWindowCollectionBehaviorFullScreenPrimary);
|
|
}
|
|
|
|
case MTMMServerConnect: // "Connect"
|
|
{
|
|
if (u == nil) {
|
|
menuItem.hidden = NO;
|
|
|
|
return NO;
|
|
}
|
|
|
|
BOOL connected = (u.isConnected || u.isConnecting);
|
|
|
|
menuItem.hidden = connected;
|
|
|
|
return (connected == NO && u.isQuitting == NO);
|
|
}
|
|
case MTMMServerConnectWithoutProxy: // "Connect Without Proxy"
|
|
{
|
|
NSUInteger flags = ([NSEvent modifierFlags] & NSEventModifierFlagDeviceIndependentFlagsMask);
|
|
|
|
if (flags != NSEventModifierFlagShift) {
|
|
menuItem.hidden = YES;
|
|
|
|
return NO;
|
|
}
|
|
|
|
if (u == nil) {
|
|
menuItem.hidden = YES;
|
|
|
|
return NO;
|
|
}
|
|
|
|
BOOL condition = (u.isConnected || u.isConnecting ||
|
|
u.config.proxyType == IRCConnectionProxyTypeNone);
|
|
|
|
menuItem.hidden = condition;
|
|
|
|
return (condition == NO && u.isQuitting == NO);
|
|
}
|
|
case MTMMServerDisconnect: // "Disconnect"
|
|
{
|
|
BOOL connected = (u.isConnected || u.isConnecting);
|
|
|
|
menuItem.hidden = (connected == NO);
|
|
|
|
return connected;
|
|
}
|
|
case MTMMServerCancelReconnect: // "Cancel Reconnect"
|
|
{
|
|
BOOL reconnecting = u.isReconnecting;
|
|
|
|
menuItem.hidden = (reconnecting == NO);
|
|
|
|
return reconnecting;
|
|
}
|
|
case MTMMServerChannelList: // "Channel List…"
|
|
{
|
|
return u.isLoggedIn;
|
|
}
|
|
case MTMMServerChangeNickname: // "Change Nickname…"
|
|
case MTWKGeneralChangeNickname: // "Change Nickname…"
|
|
{
|
|
return u.isConnected;
|
|
}
|
|
case MTMMServerDuplicateServer: // "Duplicate Server"
|
|
case MTMMServerAddChannel: // "Add Channel…"
|
|
case MTMMServerServerProperties: // "Server Properties…"
|
|
{
|
|
return (u != nil);
|
|
}
|
|
case MTMMServerDeleteServer: // "Delete Server…"
|
|
{
|
|
return (u && u.isConnecting == NO && u.isConnected == NO);
|
|
}
|
|
|
|
case MTMMNavigationNextHighlight: // "Next Highlight"
|
|
case MTMMNavigationPreviousHighlight: // "Previous Highlight"
|
|
{
|
|
TVCLogController *viewController = self.selectedViewController;
|
|
|
|
if (viewController == nil) {
|
|
return NO;
|
|
}
|
|
|
|
return [viewController highlightAvailable:(tag == MTMMNavigationPreviousHighlight)];
|
|
}
|
|
|
|
case MTMMChannelJoinChannel: // "Join Channel"
|
|
{
|
|
menuItem.hidden = (u.isLoggedIn == NO || c.isActive);
|
|
|
|
return YES;
|
|
}
|
|
case MTMMChannelLeaveChannel: // "Leave Channel"
|
|
{
|
|
menuItem.hidden = (u.isLoggedIn == NO || c.isActive == NO);
|
|
|
|
NSMenuItem *joinChannel = [menuItem.menu itemWithTag:MTMMChannelJoinChannel];
|
|
|
|
[menuItem.menu itemWithTag:MTMMChannelLeaveChannelSeparator].hidden = (menuItem.hidden && joinChannel.hidden);
|
|
|
|
return YES;
|
|
}
|
|
case MTMMChannelAddChannel: // "Add Channel…"
|
|
{
|
|
return (u != nil);
|
|
}
|
|
case MTMMChannelViewLogs: // "View Logs"
|
|
{
|
|
return [TPCPreferences logToDiskIsEnabled];
|
|
}
|
|
case MTMMChannelModifyTopic: // "Modify Topic"
|
|
case MTMMChannelModesMenu: // "Modes"
|
|
case MTMMChannelListOfBans: // "List of Bans"
|
|
{
|
|
return (u.isLoggedIn && c.isActive);
|
|
}
|
|
case MTMMChannelListOfBanExceptions: // "List of Ban Exceptions"
|
|
{
|
|
menuItem.hidden = ([u.supportInfo isListSupported:IRCISupportInfoListTypeBanException] == NO);
|
|
|
|
return (u.isLoggedIn && c.isActive);
|
|
}
|
|
case MTMMChannelListOfInviteExceptions: // "List of Invite Exceptions"
|
|
{
|
|
menuItem.hidden = ([u.supportInfo isListSupported:IRCISupportInfoListTypeInviteException] == NO);
|
|
|
|
return (u.isLoggedIn && c.isActive);
|
|
}
|
|
case MTMMChannelListOfQuiets: // "List of Quiets"
|
|
{
|
|
menuItem.hidden = ([u.supportInfo isListSupported:IRCISupportInfoListTypeQuiet] == NO);
|
|
|
|
return (u.isLoggedIn && c.isActive);
|
|
}
|
|
|
|
case MTMMQueryQueryLogs: // "Query Logs"
|
|
{
|
|
/* Query menu is used for utility windows too so we
|
|
hide "Query Logs" for anything except private messages. */
|
|
BOOL isQuery = c.isPrivateMessage;
|
|
|
|
menuItem.hidden = (isQuery == NO);
|
|
|
|
[menuItem.menu itemWithTag:MTMMQueryCloseQuerySeparator].hidden = (isQuery == NO);
|
|
|
|
return [TPCPreferences logToDiskIsEnabled];
|
|
}
|
|
|
|
case MTMMWindowToggleVisibilityOfServerList: // "Toggle Visibility of Server List"
|
|
case MTMMWindowSortChannelList: // "Sort Channel List"
|
|
case MTMMWindowCenterWindow: // "Center Window"
|
|
case MTMMWindowResetWindowToDefaultSize: // "Reset Window to Default Size"
|
|
{
|
|
BOOL isMainWindowMain = mainWindow().mainWindow;
|
|
|
|
menuItem.hidden = (isMainWindowMain == NO);
|
|
|
|
if (tag == MTMMWindowSortChannelList) {
|
|
[menuItem.menu itemWithTag:MTMMWindowSortChannelListSeparator].hidden = (isMainWindowMain == NO);
|
|
} else if (tag == MTMMWindowResetWindowToDefaultSize) {
|
|
[menuItem.menu itemWithTag:MTMMWindowResetWindowToDefaultSizeSeparator].hidden = (isMainWindowMain == NO);
|
|
}
|
|
|
|
return YES;
|
|
}
|
|
case MTMMWindowMainWindow: // "Main Window"
|
|
{
|
|
BOOL isMainWindowMain = mainWindow().mainWindow;
|
|
BOOL isMainWindowDisabled = mainWindow().disabled;
|
|
|
|
menuItem.hidden = isMainWindowMain;
|
|
|
|
return (isMainWindowDisabled == NO);
|
|
}
|
|
case MTMMWindowToggleVisibilityOfMemberList: // "Toggle Visibility of Member List"
|
|
{
|
|
BOOL isMainWindowMain = mainWindow().mainWindow;
|
|
|
|
menuItem.hidden = (isMainWindowMain == NO);
|
|
|
|
return c.isChannel;
|
|
}
|
|
case MTMMWindowToggleWindowAppearance: // "Toggle Window Appearance"
|
|
{
|
|
BOOL isMainWindowMain = mainWindow().mainWindow;
|
|
|
|
menuItem.hidden = (isMainWindowMain == NO);
|
|
|
|
[menuItem.menu itemWithTag:MTMMWindowToggleWindowAppearanceSeparator].hidden = (isMainWindowMain == NO);
|
|
|
|
return YES;
|
|
}
|
|
case MTMMWindowAddressBook: // "Address Book"
|
|
case MTMMWindowIgnoreList: // "Ignore List"
|
|
{
|
|
BOOL isMainWindowMain = mainWindow().mainWindow;
|
|
|
|
menuItem.hidden = (isMainWindowMain == NO);
|
|
|
|
return (u != nil);
|
|
}
|
|
case MTMMWindowViewLogs: // "View Logs"
|
|
{
|
|
return [TPCPreferences logToDiskIsEnabled];
|
|
}
|
|
case MTMMWindowHighlightList: // "Highlight List"
|
|
{
|
|
BOOL isMainWindowMain = mainWindow().mainWindow;
|
|
|
|
menuItem.hidden = (isMainWindowMain == NO);
|
|
|
|
if (u == nil) {
|
|
return NO;
|
|
}
|
|
|
|
return [TPCPreferences logHighlights];
|
|
}
|
|
|
|
case MTUserControlsAddIgnore: // "Add Ignore"
|
|
{
|
|
/* To make it as efficient as possible, we only check for ignore
|
|
for the "Add Ignore" menu item. When that menu item is validated,
|
|
we validate "Modify Ignore" and "Remove Ignore" at the same time. */
|
|
NSMenuItem *modifyIgnoreMenuItem = [menuItem.menu itemWithTag:MTUserControlsModifyIgnore];
|
|
NSMenuItem *removeIgnoreMenuItem = [menuItem.menu itemWithTag:MTUserControlsRemoveIgnore];
|
|
|
|
if (c.isUtility) {
|
|
modifyIgnoreMenuItem.hidden = YES;
|
|
|
|
removeIgnoreMenuItem.hidden = YES;
|
|
|
|
menuItem.hidden = NO;
|
|
|
|
return NO;
|
|
}
|
|
|
|
/* If less than or more than one user is selected, then hide all
|
|
menu items except "Add Ignore" and disable the "Add Ignore" item. */
|
|
NSArray<IRCChannelUser *> *nicknames = [self selectedMembers:menuItem];
|
|
|
|
NSString *hostmask = nicknames.firstObject.user.hostmask;
|
|
|
|
if (nicknames.count != 1 || hostmask == nil) {
|
|
modifyIgnoreMenuItem.hidden = YES;
|
|
|
|
removeIgnoreMenuItem.hidden = YES;
|
|
|
|
menuItem.hidden = NO;
|
|
|
|
return NO;
|
|
}
|
|
|
|
/* Update visibility depending on whether ignore is available */
|
|
/* When this logic was first introduced, we kept a reference to
|
|
the ignores in the represented object of the menu item.
|
|
This was stopped because information about the ignore can
|
|
change while the menu item is still open, making the object
|
|
we will reference when action is performed garbage. */
|
|
NSArray *userIgnores = [u findIgnoresForHostmask:hostmask];
|
|
|
|
BOOL condition = (userIgnores.count == 0);
|
|
|
|
modifyIgnoreMenuItem.hidden = condition;
|
|
|
|
removeIgnoreMenuItem.hidden = condition;
|
|
|
|
menuItem.hidden = (condition == NO);
|
|
|
|
return YES;
|
|
}
|
|
case MTUserControlsModifyIgnore: // "Modify Ignore"
|
|
case MTUserControlsRemoveIgnore: // "Remove Ignore"
|
|
{
|
|
return YES;
|
|
}
|
|
case MTUserControlsInviteTo: // "Invite To…"
|
|
{
|
|
if (u.isLoggedIn == NO || c.isUtility) {
|
|
return NO;
|
|
}
|
|
|
|
NSUInteger channelCount = 0;
|
|
|
|
for (IRCChannel *e in u.channelList) {
|
|
if (c != e && e.isChannel) {
|
|
channelCount++;
|
|
}
|
|
}
|
|
|
|
return (channelCount > 0);
|
|
}
|
|
case MTUserControlsGetInfo: // "Get Info (Whois)"
|
|
case MTUserControlsClientToClientMenu: // "Client-to-Client"
|
|
{
|
|
return (u.isLoggedIn && c.isUtility == NO);
|
|
}
|
|
case MTUserControlsPrivateMessage: // "Private Message (Query)"
|
|
{
|
|
menuItem.hidden = (c.isChannel == NO);
|
|
|
|
return (u.isLoggedIn && c.isUtility == NO);
|
|
}
|
|
case MTUserControlsGiveOp: // "Give Op (+o)"
|
|
case MTUserControlsGiveHalfop: // "Give Halfop (+h)"
|
|
case MTUserControlsGiveVoice: // "Give Voice (+v)"
|
|
case MTUserControlsTakeOp: // "Take Op (-o)"
|
|
case MTUserControlsTakeHalfop: // "Take Halfop (-h)"
|
|
case MTUserControlsTakeVoice: // "Take Voice (-v)"
|
|
{
|
|
return (u.isLoggedIn && c.isActive);
|
|
}
|
|
case MTUserControlsAllModesGiven: // "All Modes Given"
|
|
{
|
|
return NO;
|
|
}
|
|
case MTUserControlsAllModesTaken: // "All Modes Taken"
|
|
{
|
|
#define _setHidden(tag, value) [menuItem.menu itemWithTag:(tag)].hidden = (value)
|
|
|
|
if (c.isChannel == NO) {
|
|
_setHidden(MTUserControlsGiveOp, YES);
|
|
_setHidden(MTUserControlsGiveHalfop, YES);
|
|
_setHidden(MTUserControlsGiveVoice, YES);
|
|
_setHidden(MTUserControlsTakeOp, YES);
|
|
_setHidden(MTUserControlsTakeHalfop, YES);
|
|
_setHidden(MTUserControlsTakeVoice, YES);
|
|
|
|
_setHidden(MTUserControlsAllModesGiven, YES);
|
|
_setHidden(MTUserControlsAllModesGivenSeparator, YES);
|
|
_setHidden(MTUserControlsAllModesTaken, YES);
|
|
_setHidden(MTUserControlsAllModesTakenSeparator, YES);
|
|
|
|
return NO;
|
|
}
|
|
|
|
_setHidden(MTUserControlsAllModesGivenSeparator, NO);
|
|
_setHidden(MTUserControlsAllModesTakenSeparator, NO);
|
|
|
|
NSArray *nicknames = [self selectedMembers:menuItem];
|
|
|
|
if (nicknames.count == 1)
|
|
{
|
|
IRCChannelUser *user = nicknames[0];
|
|
|
|
IRCUserRank userRanks = user.ranks;
|
|
|
|
BOOL UserHasModeO = ((userRanks & IRCUserRankNormalOperator) == IRCUserRankNormalOperator);
|
|
BOOL UserHasModeH = NO;
|
|
BOOL UserHasModeV = ((userRanks & IRCUserRankVoiced) == IRCUserRankVoiced);
|
|
|
|
_setHidden(MTUserControlsGiveOp, UserHasModeO);
|
|
_setHidden(MTUserControlsGiveVoice, UserHasModeV);
|
|
_setHidden(MTUserControlsTakeOp, (UserHasModeO == NO));
|
|
_setHidden(MTUserControlsTakeVoice, (UserHasModeV == NO));
|
|
|
|
BOOL halfOpModeSupported = [u.supportInfo modeSymbolIsUserPrefix:@"h"];
|
|
|
|
if (halfOpModeSupported == NO) {
|
|
_setHidden(MTUserControlsGiveHalfop, YES);
|
|
_setHidden(MTUserControlsTakeHalfop, YES);
|
|
} else {
|
|
UserHasModeH = ((userRanks & IRCUserRankHalfOperator) == IRCUserRankHalfOperator);
|
|
|
|
_setHidden(MTUserControlsGiveHalfop, UserHasModeH);
|
|
_setHidden(MTUserControlsTakeHalfop, (UserHasModeH == NO));
|
|
}
|
|
|
|
BOOL hideGiveSepItem = ((UserHasModeO == NO || UserHasModeV == NO) || (UserHasModeH == NO && halfOpModeSupported));
|
|
|
|
_setHidden(MTUserControlsAllModesGiven, hideGiveSepItem);
|
|
|
|
BOOL hideTakenSepItem = (UserHasModeO || UserHasModeH || UserHasModeV);
|
|
|
|
_setHidden(MTUserControlsAllModesTaken, hideTakenSepItem);
|
|
}
|
|
else
|
|
{
|
|
_setHidden(MTUserControlsGiveOp, NO);
|
|
_setHidden(MTUserControlsGiveHalfop, NO);
|
|
_setHidden(MTUserControlsGiveVoice, NO);
|
|
_setHidden(MTUserControlsTakeOp, NO);
|
|
_setHidden(MTUserControlsTakeHalfop, NO);
|
|
_setHidden(MTUserControlsTakeVoice, NO);
|
|
|
|
_setHidden(MTUserControlsAllModesGiven, YES);
|
|
_setHidden(MTUserControlsAllModesTaken, YES);
|
|
}
|
|
|
|
return NO;
|
|
|
|
#undef _setHidden
|
|
}
|
|
case MTUserControlsBan: // "Ban"
|
|
case MTUserControlsKick: // "Kick"
|
|
case MTUserControlsBanAndKick: // "Ban and Kick"
|
|
{
|
|
BOOL isChannel = c.isChannel;
|
|
|
|
menuItem.hidden = (isChannel == NO);
|
|
|
|
[menuItem.menu itemWithTag:MTUserControlsBanAndKickSeparator].hidden = (isChannel == NO);
|
|
|
|
return (u.isLoggedIn && isChannel && c.isActive);
|
|
}
|
|
case MTUserControlsIRCOperatorMenu: // "IRC Operator"
|
|
{
|
|
menuItem.hidden = (u.userIsIRCop == NO);
|
|
|
|
return (u.isLoggedIn && c.isUtility == NO);
|
|
}
|
|
|
|
#if TEXTUAL_BUILT_WITH_ADVANCED_ENCRYPTION == 1
|
|
case MTOTRStatusButtonStartPrivateConversation:
|
|
case MTOTRStatusButtonRefreshPrivateConversation:
|
|
case MTOTRStatusButtonEndPrivateConversation:
|
|
case MTOTRStatusButtonAuthenticateChatPartner:
|
|
case MTOTRStatusButtonViewListOfFingerprints:
|
|
{
|
|
/* Even if we are not logged in, we still ask the encryption manager
|
|
to validate the menu item first so that it can hide specific menu items.
|
|
After it has done that, then we can disable if not logged in. */
|
|
if ([TPCPreferences textEncryptionIsEnabled] == NO) {
|
|
return NO;
|
|
}
|
|
|
|
if (u.isLoggedIn == NO) {
|
|
return NO;
|
|
}
|
|
|
|
BOOL valid = [sharedEncryptionManager()
|
|
validateMenuItem:menuItem
|
|
withStateOf:[u encryptionAccountNameForUser:c.name]
|
|
from:[u encryptionAccountNameForLocalUser]];
|
|
|
|
return valid;
|
|
}
|
|
#endif
|
|
|
|
case MTWKGeneralSearchWithGoogle: // "Search With Google"
|
|
{
|
|
TVCLogView *webView = self.selectedViewControllerBackingView;
|
|
|
|
if (webView == nil) {
|
|
return NO;
|
|
}
|
|
|
|
NSString *searchProviderName = [self searchProviderName];
|
|
|
|
menuItem.title = TXTLS(@"BasicLanguage[1ll-h9]", searchProviderName);
|
|
|
|
return webView.hasSelection;
|
|
}
|
|
case MTWKGeneralLookUpInDictionary: // "Look Up in Dictionary"
|
|
{
|
|
TVCLogView *webView = self.selectedViewControllerBackingView;
|
|
|
|
if (webView == nil) {
|
|
return NO;
|
|
}
|
|
|
|
NSString *selection = webView.selection;
|
|
|
|
NSUInteger selectionLength = selection.length;
|
|
|
|
if (selectionLength == 0 || selectionLength > 40) {
|
|
menuItem.title = TXTLS(@"BasicLanguage[o5l-4s]");
|
|
|
|
return NO;
|
|
}
|
|
|
|
if (selectionLength > 25) {
|
|
selection = [selection substringToIndex:24];
|
|
|
|
selection = [NSString stringWithFormat:@"%@…", selection.trim];
|
|
}
|
|
|
|
menuItem.title = TXTLS(@"BasicLanguage[zxs-yy]", selection);
|
|
|
|
return (selectionLength > 0);
|
|
}
|
|
case MTWKGeneralCopy: // "Copy" (WebView)
|
|
{
|
|
TVCLogView *webView = self.selectedViewControllerBackingView;
|
|
|
|
if (webView == nil) {
|
|
return NO;
|
|
}
|
|
|
|
return webView.hasSelection;
|
|
}
|
|
case MTWKGeneralQueryLogs: // "Query Logs" (WebKit)
|
|
{
|
|
menuItem.hidden = (c.isPrivateMessage == NO);
|
|
|
|
return [TPCPreferences logToDiskIsEnabled];
|
|
}
|
|
case MTWKGeneralChannelMenu: // "Channel" (WebKit)
|
|
{
|
|
menuItem.hidden = (c.isChannel == NO);
|
|
|
|
/* "Query Logs" will appear above this menu item,
|
|
but if this is neither channel or query, then we
|
|
have to hide the separator above that so it's not
|
|
just sitting there with nothing beneath it. */
|
|
NSMenuItem *queryLogs = [menuItem.menu itemWithTag:MTWKGeneralQueryLogs];
|
|
|
|
[menuItem.menu itemWithTag:MTWKGeneralPasteSeparator].hidden = (menuItem.hidden && queryLogs.hidden);
|
|
|
|
return YES;
|
|
}
|
|
|
|
case MTMMHelpAdvancedMenuEnableDeveloperMode: // Developer Mode
|
|
{
|
|
if ([TPCPreferences developerModeEnabled]) {
|
|
menuItem.state = NSControlStateValueOn;
|
|
} else {
|
|
menuItem.state = NSControlStateValueOff;
|
|
}
|
|
|
|
return YES;
|
|
}
|
|
|
|
case MTMainWindowSegmentedControllerAddChannel: // "Add Channel…"
|
|
{
|
|
return (u != nil);
|
|
}
|
|
|
|
default:
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
return YES;
|
|
}
|
|
|
|
- (void)menuWillOpen:(NSMenu *)menu
|
|
{
|
|
self.menuIsOpen = YES;
|
|
|
|
self.pointedClient = mainWindow().selectedClient;
|
|
self.pointedChannel = mainWindow().selectedChannel;
|
|
|
|
self.menuPerformedActionLastOpen = NO;
|
|
}
|
|
|
|
- (void)menuDidClose:(NSMenu *)menu
|
|
{
|
|
self.menuIsOpen = NO;
|
|
|
|
/* This delegate callback is received before -menuItemPerformedAction:
|
|
is called. So that our selected items can be reset if the user did
|
|
not perform an action, we call -menuClosedTimer the next time the
|
|
main queue comes around. The action is performed on the current pass
|
|
which means this prevents a race. */
|
|
XRPerformBlockAsynchronouslyOnMainQueue(^{
|
|
[self _menuClosedTimer];
|
|
});
|
|
}
|
|
|
|
- (void)_menuClosedTimer
|
|
{
|
|
if (self.menuPerformedActionLastOpen) {
|
|
return;
|
|
}
|
|
|
|
[self resetSelectedItems];
|
|
}
|
|
|
|
- (void)menuItemWillPerformedAction:(NSNotification *)aNote
|
|
{
|
|
NSMenuItem *menuItem = aNote.userInfo[@"MenuItem"];
|
|
|
|
if (menuItem.target != self) {
|
|
return;
|
|
}
|
|
|
|
self.menuPerformedActionLastOpen = YES;
|
|
}
|
|
|
|
- (void)menuItemPerformedAction:(NSNotification *)aNote
|
|
{
|
|
NSMenuItem *menuItem = aNote.userInfo[@"MenuItem"];
|
|
|
|
if (menuItem.target != self) {
|
|
return;
|
|
}
|
|
|
|
[self resetSelectedItems];
|
|
}
|
|
|
|
#pragma mark -
|
|
#pragma mark Selection
|
|
|
|
- (void)resetSelectedItems
|
|
{
|
|
self.pointedClient = nil;
|
|
self.pointedChannel = nil;
|
|
}
|
|
|
|
- (nullable IRCClient *)selectedClient
|
|
{
|
|
IRCClient *pointedClient = self.pointedClient;
|
|
|
|
if (pointedClient) {
|
|
return pointedClient;
|
|
}
|
|
|
|
return mainWindow().selectedClient;
|
|
}
|
|
|
|
- (nullable IRCChannel *)selectedChannel
|
|
{
|
|
IRCChannel *pointedChannel = self.pointedChannel;
|
|
|
|
if (pointedChannel) {
|
|
return pointedChannel;
|
|
}
|
|
|
|
return mainWindow().selectedChannel;
|
|
}
|
|
|
|
- (nullable TVCLogController *)selectedViewController
|
|
{
|
|
IRCChannel *selectedChannel = self.selectedChannel;
|
|
|
|
if (selectedChannel) {
|
|
return selectedChannel.viewController;
|
|
}
|
|
|
|
return self.selectedClient.viewController;
|
|
}
|
|
|
|
- (nullable TVCLogView *)selectedViewControllerBackingView
|
|
{
|
|
TVCLogController *viewController = self.selectedViewController;
|
|
|
|
return viewController.backingView;
|
|
}
|
|
|
|
#pragma mark -
|
|
#pragma mark Selected User(s)
|
|
|
|
- (BOOL)checkSelectedMembers:(id)sender
|
|
{
|
|
return ([self selectedMembers:sender].count > 0);
|
|
}
|
|
|
|
- (NSArray<IRCChannelUser *> *)selectedMembers:(id)sender
|
|
{
|
|
return [self selectedMembers:sender returnStrings:NO];
|
|
}
|
|
|
|
- (NSArray<NSString *> *)selectedMembersNicknames:(id)sender
|
|
{
|
|
return [self selectedMembers:sender returnStrings:YES];
|
|
}
|
|
|
|
- (NSArray *)selectedMembers:(id)sender returnStrings:(BOOL)returnStrings
|
|
{
|
|
IRCClient *u = self.selectedClient;
|
|
IRCChannel *c = self.selectedChannel;
|
|
|
|
if (u == nil || c == nil || u.isLoggedIn == NO || c.isActive == NO) {
|
|
return @[];
|
|
}
|
|
|
|
/* Return a specific nickname for WebView events */
|
|
NSString *pointedNickname = nil;
|
|
|
|
if ([sender isKindOfClass:[NSMenuItem class]]) {
|
|
pointedNickname = ((NSMenuItem *)sender).userInfo;
|
|
} else {
|
|
pointedNickname = self.pointedNickname;
|
|
}
|
|
|
|
if (pointedNickname) {
|
|
if (returnStrings) {
|
|
return @[pointedNickname];
|
|
}
|
|
|
|
IRCChannelUser *user = [c findMember:pointedNickname];
|
|
|
|
if (user) {
|
|
return @[user];
|
|
}
|
|
|
|
return @[];
|
|
}
|
|
|
|
/* If we did not have a specific nickname, then query
|
|
the user list for selected rows. */
|
|
NSMutableArray *userArray = [NSMutableArray array];
|
|
|
|
NSIndexSet *selectedRows = mainWindowMemberList().selectedRowIndexes;
|
|
|
|
[selectedRows enumerateIndexesUsingBlock:^(NSUInteger index, BOOL *stop) {
|
|
IRCChannelUser *user = [mainWindowMemberList() itemAtRow:index];
|
|
|
|
if (returnStrings) {
|
|
[userArray addObject:user.user.nickname];
|
|
} else {
|
|
[userArray addObject:user];
|
|
}
|
|
}];
|
|
|
|
return [userArray copy];
|
|
}
|
|
|
|
- (void)deselectMembers:(id)sender
|
|
{
|
|
if ([sender isKindOfClass:[NSMenuItem class]]) {
|
|
if (((NSMenuItem *)sender).userInfo.length > 0) {
|
|
return; // Nothing to deselect when our sender used userInfo
|
|
}
|
|
}
|
|
|
|
if (self.pointedNickname) {
|
|
self.pointedNickname = nil;
|
|
|
|
return;
|
|
}
|
|
|
|
[mainWindowMemberList() deselectAll:sender];
|
|
}
|
|
|
|
#pragma mark -
|
|
#pragma mark Find Panel
|
|
|
|
- (void)_showFindPromptOpenDialog:(id)sender
|
|
{
|
|
void (^promptCompletionBlock)(NSString *) = ^(NSString *resultString)
|
|
{
|
|
if ([self.currentSearchPhrase isEqualToString:resultString]) {
|
|
return;
|
|
}
|
|
|
|
self.currentSearchPhrase = resultString;
|
|
|
|
TVCLogView *webView = self.selectedViewControllerBackingView;
|
|
|
|
[webView findString:resultString movingForward:YES];
|
|
};
|
|
|
|
NSString *resultString = nil;
|
|
|
|
TVCAlertResponseButton response =
|
|
[TDCInputPrompt promptWithMessage:TXTLS(@"Prompts[d2w-4o]")
|
|
title:TXTLS(@"Prompts[akr-eh]")
|
|
defaultButton:TXTLS(@"Prompts[q5h-xx]")
|
|
alternateButton:TXTLS(@"Prompts[qso-2g]")
|
|
prefillString:self.currentSearchPhrase
|
|
resultString:&resultString];
|
|
|
|
if (response == TVCAlertResponseButtonFirst) {
|
|
promptCompletionBlock(resultString);
|
|
}
|
|
}
|
|
|
|
- (void)showFindPrompt:(id)sender
|
|
{
|
|
NSParameterAssert(sender != nil);
|
|
|
|
if (mainWindow().keyWindow == NO) {
|
|
return;
|
|
}
|
|
|
|
if ([sender tag] == MTMMEditFindMenuFind || self.currentSearchPhrase.length == 0) {
|
|
[self _showFindPromptOpenDialog:sender];
|
|
|
|
return;
|
|
}
|
|
|
|
TVCLogView *webView = self.selectedViewControllerBackingView;
|
|
|
|
if ([sender tag] == MTMMEditFindMenuFindNext) {
|
|
[webView findString:self.currentSearchPhrase movingForward:YES];
|
|
} else {
|
|
[webView findString:self.currentSearchPhrase movingForward:NO];
|
|
}
|
|
}
|
|
|
|
|
|
#pragma mark -
|
|
#pragma mark Edit
|
|
|
|
- (void)copy:(id)sender
|
|
{
|
|
id firstResponder = [NSApp keyWindow].firstResponder;
|
|
|
|
if ([firstResponder respondsToSelector:@selector(copy:)]) {
|
|
[firstResponder performSelector:@selector(copy:) withObject:sender];
|
|
}
|
|
}
|
|
|
|
- (void)paste:(id)sender
|
|
{
|
|
if (mainWindow().keyWindow) {
|
|
[mainWindowTextField() focus];
|
|
|
|
[mainWindowTextField() paste:sender];
|
|
|
|
return;
|
|
}
|
|
|
|
id firstResponder = [NSApp keyWindow].firstResponder;
|
|
|
|
if ([firstResponder respondsToSelector:@selector(paste:)]) {
|
|
[firstResponder performSelector:@selector(paste:) withObject:sender];
|
|
}
|
|
}
|
|
|
|
- (void)print:(id)sender
|
|
{
|
|
if (mainWindow().keyWindow) {
|
|
TVCLogView *webView = self.selectedViewControllerBackingView;
|
|
|
|
if (webView == nil) {
|
|
return;
|
|
}
|
|
|
|
[webView print];
|
|
|
|
return;
|
|
}
|
|
|
|
id firstResponder = [NSApp keyWindow].firstResponder;
|
|
|
|
if ([firstResponder respondsToSelector:@selector(print:)]) {
|
|
[firstResponder performSelector:@selector(print:) withObject:sender];
|
|
}
|
|
}
|
|
|
|
#pragma mark -
|
|
#pragma mark Backing View
|
|
|
|
- (void)copyLogAsHtml:(id)sender
|
|
{
|
|
TVCLogView *webView = self.selectedViewControllerBackingView;
|
|
|
|
if (webView == nil) {
|
|
return;
|
|
}
|
|
|
|
[webView copyContentString];
|
|
}
|
|
|
|
- (void)openWebInspector:(id)sender
|
|
{
|
|
/*
|
|
When WebKit2 first announced, there was no way for an
|
|
app to take an NSMenu, modify it, and return the result
|
|
when a context menu is presented.
|
|
|
|
WebKit1 had a delegate method for that named:
|
|
-webView:contextMenuItemsForElement:defaultMenuItems:
|
|
|
|
WebKit2 added this delegate method, in private, but it
|
|
was not available until the next update of macOS.
|
|
|
|
'Til then, we had to fake "Inspect Element" because we
|
|
did not have access to the default implementation that
|
|
is available through the delegate method.
|
|
|
|
Apple now flags the function names that were used to do that.
|
|
|
|
This will break "Inspect Element" on one version of macOS.
|
|
Which one I don't even recall.
|
|
But it is worth it to make the review process smoother.
|
|
|
|
Sorry.
|
|
*/
|
|
|
|
(void)
|
|
[TDCAlert alertWithMessage:TXTLS(@"Prompts[kig-m1]")
|
|
title:TXTLS(@"Prompts[ujw-64]")
|
|
defaultButton:TXTLS(@"Prompts[c7s-dq]")
|
|
alternateButton:nil];
|
|
}
|
|
|
|
- (void)markScrollback:(id)sender
|
|
{
|
|
TVCLogController *viewController = self.selectedViewController;
|
|
|
|
if (viewController == nil) {
|
|
return;
|
|
}
|
|
|
|
[viewController mark];
|
|
}
|
|
|
|
- (void)gotoScrollbackMarker:(id)sender
|
|
{
|
|
TVCLogController *viewController = self.selectedViewController;
|
|
|
|
if (viewController == nil) {
|
|
return;
|
|
}
|
|
|
|
[viewController goToMark];
|
|
}
|
|
|
|
- (void)clearScrollback:(id)sender
|
|
{
|
|
IRCClient *u = self.selectedClient;
|
|
IRCChannel *c = self.selectedChannel;
|
|
|
|
if (u == nil) {
|
|
return;
|
|
}
|
|
|
|
if (c) {
|
|
[mainWindow() clearContentsOfChannel:c];
|
|
} else {
|
|
[mainWindow() clearContentsOfClient:u];
|
|
}
|
|
}
|
|
|
|
- (void)increaseLogFontSize:(id)sender
|
|
{
|
|
[mainWindow() changeTextSize:YES];
|
|
}
|
|
|
|
- (void)decreaseLogFontSize:(id)sender
|
|
{
|
|
[mainWindow() changeTextSize:NO];
|
|
}
|
|
|
|
- (NSString *)searchProviderName
|
|
{
|
|
NSDictionary *preferredWebServices =
|
|
[[NSUserDefaults standardUserDefaults] dictionaryForKey:@"NSPreferredWebServices"];
|
|
|
|
NSDictionary *defaultSearchProvider = [preferredWebServices dictionaryForKey:@"NSWebServicesProviderWebSearch"];
|
|
|
|
NSString *searchProviderName = [defaultSearchProvider stringForKey:@"NSDefaultDisplayName"];
|
|
|
|
if (searchProviderName == nil) {
|
|
return @"Google";
|
|
}
|
|
|
|
return searchProviderName;
|
|
}
|
|
|
|
- (void)searchGoogle:(id)sender
|
|
{
|
|
TVCLogView *webView = self.selectedViewControllerBackingView;
|
|
|
|
if (webView == nil) {
|
|
return;
|
|
}
|
|
|
|
NSString *selection = webView.selection;
|
|
|
|
if (selection.length == 0) {
|
|
return;
|
|
}
|
|
|
|
NSPasteboard *searchPasteboard = [NSPasteboard pasteboardWithUniqueName];
|
|
|
|
searchPasteboard.stringContent = selection;
|
|
|
|
NSPerformService(@"Search With %WebSearchProvider@", searchPasteboard);
|
|
}
|
|
|
|
- (void)lookUpInDictionary:(id)sender
|
|
{
|
|
TVCLogView *webView = self.selectedViewControllerBackingView;
|
|
|
|
if (webView == nil) {
|
|
return;
|
|
}
|
|
|
|
NSString *selection = webView.selection;
|
|
|
|
if (selection.length == 0) {
|
|
return;
|
|
}
|
|
|
|
NSString *urlString = [NSString stringWithFormat:@"dict://%@", selection.percentEncodedString];
|
|
|
|
[TLOpenLink openWithString:urlString];
|
|
}
|
|
|
|
#pragma mark -
|
|
#pragma mark Server
|
|
|
|
- (void)connect:(id)sender
|
|
{
|
|
IRCClient *u = self.selectedClient;
|
|
|
|
if (u == nil || u.isConnecting || u.isConnected || u.isQuitting) {
|
|
return;
|
|
}
|
|
|
|
[u connect];
|
|
|
|
[mainWindow() expandClient:u];
|
|
}
|
|
|
|
- (void)connectBypassingProxy:(id)sender
|
|
{
|
|
IRCClient *u = self.selectedClient;
|
|
|
|
if (u == nil || u.isConnecting || u.isConnected || u.isQuitting) {
|
|
return;
|
|
}
|
|
|
|
[u connect:IRCClientConnectModeNormal bypassProxy:YES];
|
|
|
|
[mainWindow() expandClient:u];
|
|
}
|
|
|
|
- (void)disconnect:(id)sender
|
|
{
|
|
IRCClient *u = self.selectedClient;
|
|
|
|
if (u == nil || (u.isConnecting == NO && u.isConnected == NO) || u.isQuitting) {
|
|
return;
|
|
}
|
|
|
|
[u quit];
|
|
}
|
|
|
|
- (void)cancelReconnection:(id)sender
|
|
{
|
|
IRCClient *u = self.selectedClient;
|
|
|
|
if (u == nil) {
|
|
return;
|
|
}
|
|
|
|
[u cancelReconnect];
|
|
}
|
|
|
|
- (void)showServerChannelList:(id)sender
|
|
{
|
|
IRCClient *u = self.selectedClient;
|
|
|
|
if (u == nil || u.isLoggedIn == NO) {
|
|
return;
|
|
}
|
|
|
|
[u createChannelListDialog];
|
|
|
|
[u requestChannelList];
|
|
}
|
|
|
|
- (void)addServer:(id)sender
|
|
{
|
|
[windowController() popMainWindowSheetIfExists];
|
|
|
|
TDCServerPropertiesSheet *sheet =
|
|
[[TDCServerPropertiesSheet alloc] initWithClient:nil];
|
|
|
|
sheet.delegate = self;
|
|
|
|
sheet.window = mainWindow();
|
|
|
|
[sheet startWithSelection:TDCServerPropertiesSheetSelectionDefault context:nil];
|
|
|
|
[windowController() addWindowToWindowList:sheet];
|
|
}
|
|
|
|
- (void)duplicateServer:(id)sender
|
|
{
|
|
IRCClient *u = self.selectedClient;
|
|
|
|
if (u == nil) {
|
|
return;
|
|
}
|
|
|
|
IRCClientConfigMutable *config = [u.config uniqueCopyMutable];
|
|
|
|
config.connectionName = [config.connectionName stringByAppendingString:@"_"];
|
|
|
|
IRCClient *newClient = [worldController() createClientWithConfig:config reload:YES];
|
|
|
|
if (newClient.config.sidebarItemExpanded) { // Only expand new client if old was expanded already.
|
|
[mainWindow() expandClient:newClient];
|
|
}
|
|
|
|
[worldController() save];
|
|
}
|
|
|
|
- (void)deleteServer:(id)sender
|
|
{
|
|
IRCClient *u = self.selectedClient;
|
|
|
|
if (u == nil || u.isConnecting || u.isConnected) {
|
|
return;
|
|
}
|
|
|
|
BOOL result = [TDCAlert modalAlertWithMessage:TXTLS(@"Prompts[etl-ss]")
|
|
title:TXTLS(@"Prompts[0kz-wd]")
|
|
defaultButton:TXTLS(@"Prompts[mvh-ms]")
|
|
alternateButton:TXTLS(@"Prompts[99q-gg]")];
|
|
|
|
if (result == NO) {
|
|
return;
|
|
}
|
|
|
|
[worldController() destroyClient:u];
|
|
|
|
[worldController() save];
|
|
}
|
|
|
|
#pragma mark -
|
|
#pragma mark Channel
|
|
|
|
- (void)joinChannel:(id)sender
|
|
{
|
|
IRCClient *u = self.selectedClient;
|
|
IRCChannel *c = self.selectedChannel;
|
|
|
|
if (u == nil || c == nil || u.isLoggedIn == NO || c.isChannel == NO || c.isActive) {
|
|
return;
|
|
}
|
|
|
|
[u joinChannel:c];
|
|
|
|
[mainWindow() select:c];
|
|
}
|
|
|
|
- (void)leaveChannel:(id)sender
|
|
{
|
|
IRCClient *u = self.selectedClient;
|
|
IRCChannel *c = self.selectedChannel;
|
|
|
|
/* Second boxed in condition is because this is used for queries too */
|
|
if (u == nil || c == nil || (c.isChannel && (u.isLoggedIn == NO || c.isActive == NO))) {
|
|
return;
|
|
}
|
|
|
|
if (c.isChannel) {
|
|
[u partChannel:c];
|
|
|
|
return;
|
|
}
|
|
|
|
[worldController() destroyChannel:c];
|
|
}
|
|
|
|
- (void)addChannel:(id)sender
|
|
{
|
|
[windowController() popMainWindowSheetIfExists];
|
|
|
|
IRCClient *u = self.selectedClient;
|
|
|
|
if (u == nil) {
|
|
return;
|
|
}
|
|
|
|
TDCChannelPropertiesSheet *sheet =
|
|
[[TDCChannelPropertiesSheet alloc] initWithClient:u];
|
|
|
|
sheet.delegate = self;
|
|
|
|
sheet.window = mainWindow();
|
|
|
|
[sheet start];
|
|
|
|
[windowController() addWindowToWindowList:sheet];
|
|
}
|
|
|
|
- (void)deleteChannel:(id)sender
|
|
{
|
|
IRCChannel *c = self.selectedChannel;
|
|
|
|
if (c == nil) {
|
|
return;
|
|
}
|
|
|
|
if (c.isChannel) {
|
|
BOOL result = [TDCAlert modalAlertWithMessage:TXTLS(@"Prompts[516-ms]")
|
|
title:TXTLS(@"Prompts[i8o-7z]")
|
|
defaultButton:TXTLS(@"Prompts[mvh-ms]")
|
|
alternateButton:TXTLS(@"Prompts[99q-gg]")
|
|
suppressionKey:@"delete_channel"
|
|
suppressionText:nil];
|
|
|
|
if (result == NO) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
[worldController() destroyChannel:c];
|
|
|
|
[worldController() save];
|
|
}
|
|
|
|
- (void)copyUniqueIdentifier:(id)sender
|
|
{
|
|
IRCChannel *c = self.selectedChannel;
|
|
|
|
if (c == nil) {
|
|
return;
|
|
}
|
|
|
|
[RZPasteboard() setStringContent:c.uniqueIdentifier];
|
|
}
|
|
|
|
#pragma mark -
|
|
#pragma mark Other Actions
|
|
|
|
- (void)copyUrl:(id)sender
|
|
{
|
|
NSString *pointedUrl = ((NSMenuItem *)sender).userInfo;
|
|
|
|
if (pointedUrl.length == 0) {
|
|
return;
|
|
}
|
|
|
|
RZPasteboard().stringContent = pointedUrl;
|
|
}
|
|
|
|
- (void)joinChannelClicked:(id)sender
|
|
{
|
|
NSParameterAssert(sender != nil);
|
|
|
|
IRCClient *u = self.selectedClient;
|
|
|
|
if (u == nil || u.isLoggedIn == NO) {
|
|
return;
|
|
}
|
|
|
|
NSString *pointedChannelName = nil;
|
|
|
|
if ([sender isKindOfClass:[NSMenuItem class]]) {
|
|
pointedChannelName = ((NSMenuItem *)sender).userInfo;
|
|
} else if ([sender isKindOfClass:[NSString class]]) {
|
|
pointedChannelName = sender;
|
|
} else {
|
|
return;
|
|
}
|
|
|
|
if ([u stringIsChannelName:pointedChannelName] == NO) {
|
|
return;
|
|
}
|
|
|
|
IRCChannel *c = [u findChannelOrCreate:pointedChannelName];
|
|
|
|
[u joinChannel:c];
|
|
|
|
[mainWindow() select:c];
|
|
}
|
|
|
|
- (void)emptyAction:(id)sender
|
|
{
|
|
/* Empty action used to validate submenus */
|
|
}
|
|
|
|
#pragma mark -
|
|
#pragma mark Ignores
|
|
|
|
- (void)memberAddIgnore:(id)sender
|
|
{
|
|
IRCClient *u = self.selectedClient;
|
|
IRCChannel *c = self.selectedChannel;
|
|
|
|
if (u == nil || c == nil) {
|
|
return;
|
|
}
|
|
|
|
NSArray *nicknames = [self selectedMembersNicknames:sender];
|
|
|
|
if (nicknames.count == 0) {
|
|
return;
|
|
}
|
|
|
|
[self deselectMembers:sender];
|
|
|
|
NSString *command = [NSString stringWithFormat:@"ignore %@", nicknames[0]];
|
|
|
|
[u sendCommand:command completeTarget:YES target:c.name];
|
|
}
|
|
|
|
- (void)memberRemoveIgnore:(id)sender
|
|
{
|
|
IRCClient *u = self.selectedClient;
|
|
IRCChannel *c = self.selectedChannel;
|
|
|
|
if (u == nil || c == nil) {
|
|
return;
|
|
}
|
|
|
|
NSArray *nicknames = [self selectedMembersNicknames:sender];
|
|
|
|
if (nicknames.count == 0) {
|
|
return;
|
|
}
|
|
|
|
[self deselectMembers:sender];
|
|
|
|
NSString *command = [NSString stringWithFormat:@"unignore %@", nicknames[0]];
|
|
|
|
[u sendCommand:command completeTarget:YES target:c.name];
|
|
}
|
|
|
|
- (void)memberModifyIgnore:(id)sender
|
|
{
|
|
IRCClient *u = self.selectedClient;
|
|
|
|
if (u == nil) {
|
|
return;
|
|
}
|
|
|
|
NSArray<IRCChannelUser *> *nicknames = [self selectedMembers:sender];
|
|
|
|
[self deselectMembers:sender];
|
|
|
|
/* User's hostmask and other information can change between the point
|
|
the menu item is opened and the point the action is performed.
|
|
We therefore perform a new query for ignores when performing action. */
|
|
NSString *hostmask = nicknames.firstObject.user.hostmask;
|
|
|
|
if (nicknames.count != 1 || hostmask == nil) {
|
|
return;
|
|
}
|
|
|
|
NSArray *userIgnores = [u findIgnoresForHostmask:hostmask];
|
|
|
|
/* If we have more than one user ignore, then open
|
|
the address book instead of a specific ignore. */
|
|
if (userIgnores.count == 1) {
|
|
[self showServerPropertiesSheetForClient:u
|
|
withSelection:TDCServerPropertiesSheetSelectionNewIgnoreEntry
|
|
context:userIgnores[0]];
|
|
} else {
|
|
[self showServerPropertiesSheetForClient:u
|
|
withSelection:TDCServerPropertiesSheetSelectionAddressBook
|
|
context:nil];
|
|
}
|
|
}
|
|
|
|
#pragma mark -
|
|
#pragma mark Members
|
|
|
|
- (void)memberInMemberListDoubleClicked:(id)sender
|
|
{
|
|
NSInteger rowBeneathMouse = mainWindowMemberList().rowBeneathMouse;
|
|
|
|
if (rowBeneathMouse < 0) {
|
|
return;
|
|
}
|
|
|
|
TXUserDoubleClickAction action = [TPCPreferences userDoubleClickOption];
|
|
|
|
if (action == TXUserDoubleClickActionWhois) {
|
|
[self whoisSelectedMembers:sender];
|
|
} else if (action == TXUserDoubleClickActionPrivateMessage) {
|
|
[self memberStartPrivateMessage:sender];
|
|
} else if (action == TXUserDoubleClickActionInsertTextField) {
|
|
[self memberInsertNameIntoTextField:sender];
|
|
}
|
|
}
|
|
|
|
- (void)memberInChannelViewDoubleClicked:(id)sender
|
|
{
|
|
TXUserDoubleClickAction action = [TPCPreferences userDoubleClickOption];
|
|
|
|
if (action == TXUserDoubleClickActionWhois) {
|
|
[self whoisSelectedMembers:sender];
|
|
} else if (action == TXUserDoubleClickActionPrivateMessage) {
|
|
[self memberStartPrivateMessage:sender];
|
|
} else if (action == TXUserDoubleClickActionInsertTextField) {
|
|
[self memberInsertNameIntoTextField:sender];
|
|
}
|
|
}
|
|
|
|
- (void)memberInsertNameIntoTextField:(id)sender
|
|
{
|
|
IRCClient *u = self.selectedClient;
|
|
IRCChannel *c = self.selectedChannel;
|
|
|
|
if (u == nil || c == nil) {
|
|
return;
|
|
}
|
|
|
|
NSArray *nicknames = [self selectedMembersNicknames:sender];
|
|
|
|
if (nicknames.count == 0) {
|
|
return;
|
|
}
|
|
|
|
[self deselectMembers:sender];
|
|
|
|
TVCMainWindowTextView *textView = mainWindowTextField();
|
|
|
|
NSRange selectedRange = textView.selectedRange;
|
|
|
|
NSMutableString *stringToInsert = [NSMutableString string];
|
|
|
|
if (selectedRange.location > 0) {
|
|
UniChar previousCharacter = [textView.stringValue characterAtIndex:(selectedRange.location - 1)];
|
|
|
|
if ([[NSCharacterSet whitespaceCharacterSet] characterIsMember:previousCharacter] == NO) {
|
|
[stringToInsert appendString:@" "];
|
|
}
|
|
}
|
|
|
|
NSString *nicknamesString = [nicknames componentsJoinedByString:@", "];
|
|
|
|
[stringToInsert appendString:nicknamesString];
|
|
|
|
NSString *completionSuffix = [TPCPreferences tabCompletionSuffix];
|
|
|
|
if (completionSuffix != nil) {
|
|
[stringToInsert appendString:completionSuffix];
|
|
}
|
|
|
|
[textView replaceCharactersInRange:selectedRange withString:stringToInsert];
|
|
|
|
[textView resetFontColorInRange:selectedRange];
|
|
|
|
[textView focus];
|
|
}
|
|
|
|
- (void)memberSendWhois:(id)sender
|
|
{
|
|
[self whoisSelectedMembers:sender];
|
|
}
|
|
|
|
- (void)whoisSelectedMembers:(id)sender
|
|
{
|
|
IRCClient *u = self.selectedClient;
|
|
IRCChannel *c = self.selectedChannel;
|
|
|
|
if (u == nil || c == nil) {
|
|
return;
|
|
}
|
|
|
|
for (NSString *nickname in [self selectedMembersNicknames:sender]) {
|
|
[u sendWhois:nickname];
|
|
}
|
|
|
|
[self deselectMembers:sender];
|
|
}
|
|
|
|
- (void)memberStartPrivateMessage:(id)sender
|
|
{
|
|
IRCClient *u = self.selectedClient;
|
|
IRCChannel *c = self.selectedChannel;
|
|
|
|
if (u == nil || c == nil) {
|
|
return;
|
|
}
|
|
|
|
for (NSString *nickname in [self selectedMembersNicknames:sender]) {
|
|
IRCChannel *query = [u findChannelOrCreate:nickname isPrivateMessage:YES];
|
|
|
|
[mainWindow() select:query];
|
|
}
|
|
|
|
[self deselectMembers:sender];
|
|
}
|
|
|
|
- (void)memberSendCTCPPing:(id)sender
|
|
{
|
|
IRCClient *u = self.selectedClient;
|
|
IRCChannel *c = self.selectedChannel;
|
|
|
|
if (u == nil || c == nil) {
|
|
return;
|
|
}
|
|
|
|
for (NSString *nickname in [self selectedMembersNicknames:sender]) {
|
|
[u sendCTCPPing:nickname];
|
|
}
|
|
|
|
[self deselectMembers:sender];
|
|
}
|
|
|
|
- (void)memberSendCTCPFinger:(id)sender
|
|
{
|
|
IRCClient *u = self.selectedClient;
|
|
IRCChannel *c = self.selectedChannel;
|
|
|
|
if (u == nil || c == nil) {
|
|
return;
|
|
}
|
|
|
|
for (NSString *nickname in [self selectedMembersNicknames:sender]) {
|
|
[u sendCTCPQuery:nickname command:@"FINGER" text:nil];
|
|
}
|
|
|
|
[self deselectMembers:sender];
|
|
}
|
|
|
|
- (void)memberSendCTCPTime:(id)sender
|
|
{
|
|
IRCClient *u = self.selectedClient;
|
|
IRCChannel *c = self.selectedChannel;
|
|
|
|
if (u == nil || c == nil) {
|
|
return;
|
|
}
|
|
|
|
for (NSString *nickname in [self selectedMembersNicknames:sender]) {
|
|
[u sendCTCPQuery:nickname command:@"TIME" text:nil];
|
|
}
|
|
|
|
[self deselectMembers:sender];
|
|
}
|
|
|
|
- (void)memberSendCTCPVersion:(id)sender
|
|
{
|
|
IRCClient *u = self.selectedClient;
|
|
IRCChannel *c = self.selectedChannel;
|
|
|
|
if (u == nil || c == nil) {
|
|
return;
|
|
}
|
|
|
|
for (NSString *nickname in [self selectedMembersNicknames:sender]) {
|
|
[u sendCTCPQuery:nickname command:@"VERSION" text:nil];
|
|
}
|
|
|
|
[self deselectMembers:sender];
|
|
}
|
|
|
|
- (void)memberSendCTCPUserinfo:(id)sender
|
|
{
|
|
IRCClient *u = self.selectedClient;
|
|
IRCChannel *c = self.selectedChannel;
|
|
|
|
if (u == nil || c == nil) {
|
|
return;
|
|
}
|
|
|
|
for (NSString *nickname in [self selectedMembersNicknames:sender]) {
|
|
[u sendCTCPQuery:nickname command:@"USERINFO" text:nil];
|
|
}
|
|
|
|
[self deselectMembers:sender];
|
|
}
|
|
|
|
- (void)memberSendCTCPClientInfo:(id)sender
|
|
{
|
|
IRCClient *u = self.selectedClient;
|
|
IRCChannel *c = self.selectedChannel;
|
|
|
|
if (u == nil || c == nil) {
|
|
return;
|
|
}
|
|
|
|
for (NSString *nickname in [self selectedMembersNicknames:sender]) {
|
|
[u sendCTCPQuery:nickname command:@"CLIENTINFO" text:nil];
|
|
}
|
|
|
|
[self deselectMembers:sender];
|
|
}
|
|
|
|
- (void)_processModeChange:(id)sender usingCommand:(NSString *)modeCommand
|
|
{
|
|
IRCClient *u = self.selectedClient;
|
|
IRCChannel *c = self.selectedChannel;
|
|
|
|
if (u == nil || c == nil || u.isLoggedIn == NO || c.isChannel == NO) {
|
|
return;
|
|
}
|
|
|
|
NSArray *nicknames = [self selectedMembersNicknames:sender];
|
|
|
|
[self deselectMembers:sender];
|
|
|
|
NSString *nicknamesString = [nicknames componentsJoinedByString:@" "];
|
|
|
|
NSString *command = [NSString stringWithFormat:@"%@ %@", modeCommand, nicknamesString];
|
|
|
|
[u sendCommand:command completeTarget:YES target:c.name];
|
|
}
|
|
|
|
- (void)memberModeGiveOp:(id)sender
|
|
{
|
|
[self _processModeChange:sender usingCommand:@"OP"];
|
|
}
|
|
|
|
- (void)memberModeTakeOp:(id)sender
|
|
{
|
|
[self _processModeChange:sender usingCommand:@"DEOP"];
|
|
}
|
|
|
|
- (void)memberModeGiveHalfop:(id)sender
|
|
{
|
|
[self _processModeChange:sender usingCommand:@"HALFOP"];
|
|
}
|
|
|
|
- (void)memberModeTakeHalfop:(id)sender
|
|
{
|
|
[self _processModeChange:sender usingCommand:@"DEHALFOP"];
|
|
}
|
|
|
|
- (void)memberModeGiveVoice:(id)sender
|
|
{
|
|
[self _processModeChange:sender usingCommand:@"VOICE"];
|
|
}
|
|
|
|
- (void)memberModeTakeVoice:(id)sender
|
|
{
|
|
[self _processModeChange:sender usingCommand:@"DEVOICE"];
|
|
}
|
|
|
|
- (void)memberKickFromChannel:(id)sender
|
|
{
|
|
IRCClient *u = self.selectedClient;
|
|
IRCChannel *c = self.selectedChannel;
|
|
|
|
if (u == nil || c == nil || u.isLoggedIn == NO || c.isChannel == NO) {
|
|
return;
|
|
}
|
|
|
|
for (NSString *nickname in [self selectedMembersNicknames:sender]) {
|
|
[u kick:nickname inChannel:c];
|
|
}
|
|
|
|
[self deselectMembers:sender];
|
|
}
|
|
|
|
- (void)memberBanFromChannel:(id)sender
|
|
{
|
|
IRCClient *u = self.selectedClient;
|
|
IRCChannel *c = self.selectedChannel;
|
|
|
|
if (u == nil || c == nil || u.isLoggedIn == NO || c.isChannel == NO) {
|
|
return;
|
|
}
|
|
|
|
for (NSString *nickname in [self selectedMembersNicknames:sender]) {
|
|
NSString *command = [NSString stringWithFormat:@"BAN %@", nickname];
|
|
|
|
[u sendCommand:command completeTarget:YES target:c.name];
|
|
}
|
|
|
|
[self deselectMembers:sender];
|
|
}
|
|
|
|
- (void)memberKickbanFromChannel:(id)sender
|
|
{
|
|
IRCClient *u = self.selectedClient;
|
|
IRCChannel *c = self.selectedChannel;
|
|
|
|
if (u == nil || c == nil || u.isLoggedIn == NO || c.isChannel == NO) {
|
|
return;
|
|
}
|
|
|
|
for (NSString *nickname in [self selectedMembersNicknames:sender]) {
|
|
NSString *command = [NSString stringWithFormat:@"KICKBAN %@ %@", nickname, [TPCPreferences defaultKickMessage]];
|
|
|
|
[u sendCommand:command completeTarget:YES target:c.name];
|
|
}
|
|
|
|
[self deselectMembers:sender];
|
|
}
|
|
|
|
- (void)memberKillFromServer:(id)sender
|
|
{
|
|
IRCClient *u = self.selectedClient;
|
|
IRCChannel *c = self.selectedChannel;
|
|
|
|
if (u == nil || c == nil || u.isLoggedIn == NO) {
|
|
return;
|
|
}
|
|
|
|
for (NSString *nickname in [self selectedMembersNicknames:sender]) {
|
|
NSString *command = [NSString stringWithFormat:@"KILL %@ %@", nickname, [TPCPreferences IRCopDefaultKillMessage]];
|
|
|
|
[u sendCommand:command];
|
|
}
|
|
|
|
[self deselectMembers:sender];
|
|
}
|
|
|
|
- (void)memberBanFromServer:(id)sender
|
|
{
|
|
IRCClient *u = self.selectedClient;
|
|
IRCChannel *c = self.selectedChannel;
|
|
|
|
if (u == nil || c == nil || u.isLoggedIn == NO) {
|
|
return;
|
|
}
|
|
|
|
for (NSString *nickname in [self selectedMembersNicknames:sender]) {
|
|
if ([u nicknameIsMyself:nickname]) {
|
|
[u printDebugInformation:TXTLS(@"IRC[0r1-5l]", u.serverAddress) inChannel:c];
|
|
|
|
continue;
|
|
}
|
|
|
|
NSString *command = [NSString stringWithFormat:@"GLINE %@ %@", nickname, [TPCPreferences IRCopDefaultGlineMessage]];
|
|
|
|
[u sendCommand:command];
|
|
}
|
|
|
|
[self deselectMembers:sender];
|
|
}
|
|
|
|
- (void)memberShunOnServer:(id)sender
|
|
{
|
|
IRCClient *u = self.selectedClient;
|
|
IRCChannel *c = self.selectedChannel;
|
|
|
|
if (u == nil || c == nil || u.isLoggedIn == NO) {
|
|
return;
|
|
}
|
|
|
|
for (NSString *nickname in [self selectedMembersNicknames:sender]) {
|
|
NSString *command = [NSString stringWithFormat:@"SHUN %@ %@", nickname, [TPCPreferences IRCopDefaultShunMessage]];
|
|
|
|
[u sendCommand:command];
|
|
}
|
|
|
|
[self deselectMembers:sender];
|
|
}
|
|
|
|
- (void)_showSetVhostPromptOpenDialog:(id)sender
|
|
{
|
|
IRCClient *u = self.selectedClient;
|
|
IRCChannel *c = self.selectedChannel;
|
|
|
|
if (u == nil || c == nil || u.isLoggedIn == NO) {
|
|
return;
|
|
}
|
|
|
|
NSArray *nicknames = [self selectedMembersNicknames:sender];
|
|
|
|
if (nicknames.count == 0) {
|
|
return;
|
|
}
|
|
|
|
[self deselectMembers:sender];
|
|
|
|
void (^promptCompletionBlock)(NSString *) = ^(NSString *resultString)
|
|
{
|
|
NSString *vhost = resultString.trimAndGetFirstToken;
|
|
|
|
if (vhost.length == 0) {
|
|
return;
|
|
}
|
|
|
|
for (NSString *nickname in nicknames) {
|
|
NSString *command = [NSString stringWithFormat:@"hs setall %@ %@", nickname, vhost];
|
|
|
|
[u sendCommand:command completeTarget:NO target:nil];
|
|
}
|
|
};
|
|
|
|
NSString *vhost = nil;
|
|
|
|
TVCAlertResponseButton response =
|
|
[TDCInputPrompt promptWithMessage:TXTLS(@"Prompts[2mx-jf]")
|
|
title:TXTLS(@"Prompts[7gr-e4]")
|
|
defaultButton:TXTLS(@"Prompts[c7s-dq]")
|
|
alternateButton:TXTLS(@"Prompts[qso-2g]")
|
|
prefillString:nil
|
|
resultString:&vhost];
|
|
|
|
if (response == TVCAlertResponseButtonFirst) {
|
|
promptCompletionBlock(vhost);
|
|
}
|
|
}
|
|
|
|
- (void)showSetVhostPrompt:(id)sender
|
|
{
|
|
[self _showSetVhostPromptOpenDialog:sender];
|
|
}
|
|
|
|
#pragma mark -
|
|
#pragma mark File Transfers
|
|
|
|
- (TDCFileTransferDialog *)fileTransferController
|
|
{
|
|
return [TXSharedApplication sharedFileTransferDialog];
|
|
}
|
|
|
|
- (void)showFileTransfersWindow:(id)sender
|
|
{
|
|
[self.fileTransferController show:YES restorePosition:YES];
|
|
}
|
|
|
|
- (void)memberSendFileRequest:(id)sender
|
|
{
|
|
IRCClient *u = self.selectedClient;
|
|
IRCChannel *c = self.selectedChannel;
|
|
|
|
if (u == nil || c == nil || u.isLoggedIn == NO) {
|
|
return;
|
|
}
|
|
|
|
NSArray *nicknames = [self selectedMembersNicknames:sender];
|
|
|
|
if (nicknames.count == 0) {
|
|
return;
|
|
}
|
|
|
|
[self deselectMembers:sender];
|
|
|
|
NSOpenPanel *openPanel = [NSOpenPanel openPanel];
|
|
|
|
openPanel.allowsMultipleSelection = YES;
|
|
openPanel.canChooseDirectories = NO;
|
|
openPanel.canChooseFiles = YES;
|
|
openPanel.canCreateDirectories = NO;
|
|
openPanel.resolvesAliases = YES;
|
|
|
|
[openPanel beginSheetModalForWindow:mainWindow() completionHandler:^(NSInteger returnCode) {
|
|
if (returnCode != NSModalResponseOK) {
|
|
return;
|
|
}
|
|
|
|
[self.fileTransferController.fileTransferTable beginUpdates];
|
|
|
|
for (NSString *nickname in nicknames) {
|
|
for (NSURL *path in openPanel.URLs) {
|
|
[self.fileTransferController addSenderForClient:u nickname:nickname path:path.path autoOpen:YES];
|
|
}
|
|
}
|
|
|
|
[self.fileTransferController.fileTransferTable endUpdates];
|
|
}];
|
|
}
|
|
|
|
- (void)memberSendDroppedFilesToSelectedChannel:(NSArray<NSString *> *)files
|
|
{
|
|
IRCClient *u = self.selectedClient;
|
|
IRCChannel *c = self.selectedChannel;
|
|
|
|
if (u == nil || c == nil || u.isLoggedIn == NO || c.isPrivateMessage == NO) {
|
|
return;
|
|
}
|
|
|
|
[self memberSendDroppedFiles:files to:c.name];
|
|
}
|
|
|
|
- (void)memberSendDroppedFiles:(NSArray<NSString *> *)files row:(NSUInteger)row
|
|
{
|
|
IRCClient *u = self.selectedClient;
|
|
IRCChannel *c = self.selectedChannel;
|
|
|
|
if (u == nil || c == nil || u.isLoggedIn == NO || c.isPrivateMessage == NO) {
|
|
return;
|
|
}
|
|
|
|
IRCChannelUser *member = [mainWindowMemberList() itemAtRow:row];
|
|
|
|
[self memberSendDroppedFiles:files to:member.user.nickname];
|
|
}
|
|
|
|
- (void)memberSendDroppedFiles:(NSArray<NSString *> *)files to:(NSString *)nickname
|
|
{
|
|
NSParameterAssert(files != nil);
|
|
NSParameterAssert(nickname != nil);
|
|
|
|
IRCClient *u = self.selectedClient;
|
|
|
|
if (u == nil || u.isLoggedIn == NO) {
|
|
return;
|
|
}
|
|
|
|
[self.fileTransferController.fileTransferTable beginUpdates];
|
|
|
|
for (NSString *file in files) {
|
|
BOOL isDirectory = NO;
|
|
|
|
if ([RZFileManager() fileExistsAtPath:file isDirectory:&isDirectory] == NO) {
|
|
continue;
|
|
} else if (isDirectory) {
|
|
continue;
|
|
}
|
|
|
|
[self.fileTransferController addSenderForClient:u nickname:nickname path:file autoOpen:YES];
|
|
}
|
|
|
|
[self.fileTransferController.fileTransferTable endUpdates];
|
|
}
|
|
|
|
#pragma mark -
|
|
#pragma mark Logging
|
|
|
|
- (void)openLogLocation:(id)sender
|
|
{
|
|
NSURL *path = [TPCPathInfo transcriptFolderURL];
|
|
|
|
if (path == nil) {
|
|
return;
|
|
}
|
|
|
|
if ([RZFileManager() fileExistsAtURL:path]) {
|
|
[RZWorkspace() openURL:path];
|
|
|
|
return;
|
|
}
|
|
|
|
[TDCAlert modalAlertWithMessage:TXTLS(@"Prompts[f05-hu]")
|
|
title:TXTLS(@"Prompts[k55-19]")
|
|
defaultButton:TXTLS(@"Prompts[c7s-dq]")
|
|
alternateButton:nil];
|
|
}
|
|
|
|
- (void)openChannelLogs:(id)sender
|
|
{
|
|
IRCClient *u = self.selectedClient;
|
|
IRCChannel *c = self.selectedChannel;
|
|
|
|
if (u == nil || c == nil) {
|
|
return;
|
|
}
|
|
|
|
NSURL *path = c.logFilePath;
|
|
|
|
if (path == nil) {
|
|
return;
|
|
}
|
|
|
|
if ([RZFileManager() fileExistsAtURL:path]) {
|
|
[RZWorkspace() openURL:path];
|
|
|
|
return;
|
|
}
|
|
|
|
[TDCAlert modalAlertWithMessage:TXTLS(@"Prompts[f05-hu]")
|
|
title:TXTLS(@"Prompts[k55-19]")
|
|
defaultButton:TXTLS(@"Prompts[c7s-dq]")
|
|
alternateButton:nil];
|
|
}
|
|
|
|
#pragma mark -
|
|
#pragma mark Help
|
|
|
|
- (void)openAcknowledgements:(id)sender
|
|
{
|
|
NSURL *Acknowledgements = [RZMainBundle() URLForResource:@"Acknowledgements" withExtension:@"pdf" subdirectory:@"Documentation"];
|
|
|
|
[RZWorkspace() openURL:Acknowledgements];
|
|
}
|
|
|
|
- (void)openHelpMenuItem:(id)sender
|
|
{
|
|
NSParameterAssert(sender != nil);
|
|
|
|
NSDictionary *_helpMenuLinks = @{
|
|
@(MTMMHelpLicenseAgreement) : @"https://help.codeux.com/textual/End-User-License-Agreement.kb",
|
|
@(MTMMHelpPrivacyPolicy) : @"https://help.codeux.com/textual/Privacy-Policy.kb",
|
|
@(MTMMHelpFrequentlyAskedQuestions) : @"https://help.codeux.com/textual/Frequently-Asked-Questions.kb",
|
|
@(MTMMHelpKBMenuKnowledgeBaseHome) : @"https://help.codeux.com/textual/home.kb",
|
|
@(MTMMHelpKBMenuChatEncryption) : @"https://help.codeux.com/textual/Off-the-Record-Messaging.kb",
|
|
@(MTMMHelpKBMenuCommandReference) : @"https://help.codeux.com/textual/Command-Reference.kb",
|
|
@(MTMMHelpKBMenuFeatureRequests) : @"https://help.codeux.com/textual/Support.kb",
|
|
@(MTMMHelpKBMenuKeyboardShortcuts) : @"https://help.codeux.com/textual/Keyboard-Shortcuts.kb",
|
|
@(MTMMHelpKBMenuMemoryManagement) : @"https://help.codeux.com/textual/Memory-Management.kb",
|
|
@(MTMMHelpKBMenuNetworkTimeouts) : @"https://help.codeux.com/textual/Network-Timeouts.kb",
|
|
@(MTMMHelpKBMenuTextFormatting) : @"https://help.codeux.com/textual/Text-Formatting.kb",
|
|
@(MTMMHelpKBMenuStylingInformation) : @"https://help.codeux.com/textual/Styles.kb",
|
|
@(MTMMHelpKBMenuConnectingWithCertificate) : @"https://help.codeux.com/textual/Using-CertFP.kb",
|
|
@(MTMMHelpKBMenuConnectingToBouncer) : @"https://help.codeux.com/textual/Connecting-to-ZNC-Bouncer.kb",
|
|
@(MTMMHelpKBMenuDCCFileTransferInformation) : @"https://help.codeux.com/textual/DCC-File-Transfer-Information.kb"
|
|
};
|
|
|
|
NSString *link = _helpMenuLinks[@([sender tag])];
|
|
|
|
[TLOpenLink openWithString:link inBackground:NO];
|
|
}
|
|
|
|
- (void)openStandaloneStoreWebpage:(id)sender
|
|
{
|
|
[TLOpenLink openWithString:@"https://www.textualapp.com/standalone-store" inBackground:NO];
|
|
}
|
|
|
|
- (void)contactSupport:(id)sender
|
|
{
|
|
[TLOpenLink openWithString:@"https://contact.codeux.com/" inBackground:NO];
|
|
}
|
|
|
|
- (void)connectToTextualHelpChannel:(id)sender
|
|
{
|
|
[IRCExtras createConnectionToServer:@"irc.libera.chat +6697" channelList:@"#textual" connectWhenCreated:YES mergeConnectionIfPossible:YES selectFirstChannelAdded:YES];
|
|
}
|
|
|
|
- (void)connectToTextualTestingChannel:(id)sender
|
|
{
|
|
[IRCExtras createConnectionToServer:@"irc.libera.chat +6697" channelList:@"#textual-testing" connectWhenCreated:YES mergeConnectionIfPossible:YES selectFirstChannelAdded:YES];
|
|
}
|
|
|
|
#pragma mark -
|
|
#pragma mark IRC
|
|
|
|
- (void)showChannelBanList:(id)sender
|
|
{
|
|
IRCClient *u = self.selectedClient;
|
|
IRCChannel *c = self.selectedChannel;
|
|
|
|
if (u == nil || c == nil || u.isLoggedIn == NO || c.isChannel == NO) {
|
|
return;
|
|
}
|
|
|
|
[u createChannelBanListSheet];
|
|
|
|
[u sendModes:@"+b" withParameters:nil inChannel:c];
|
|
}
|
|
|
|
- (void)showChannelBanExceptionList:(id)sender
|
|
{
|
|
IRCClient *u = self.selectedClient;
|
|
IRCChannel *c = self.selectedChannel;
|
|
|
|
if (u == nil || c == nil || u.isLoggedIn == NO || c.isChannel == NO) {
|
|
return;
|
|
}
|
|
|
|
[u createChannelBanExceptionListSheet];
|
|
|
|
[u sendModes:@"+e" withParameters:nil inChannel:c];
|
|
}
|
|
|
|
- (void)showChannelInviteExceptionList:(id)sender
|
|
{
|
|
IRCClient *u = self.selectedClient;
|
|
IRCChannel *c = self.selectedChannel;
|
|
|
|
if (u == nil || c == nil || u.isLoggedIn == NO || c.isChannel == NO) {
|
|
return;
|
|
}
|
|
|
|
[u createChannelInviteExceptionListSheet];
|
|
|
|
[u sendModes:@"+I" withParameters:nil inChannel:c];
|
|
}
|
|
|
|
- (void)showChannelQuietList:(id)sender
|
|
{
|
|
IRCClient *u = self.selectedClient;
|
|
IRCChannel *c = self.selectedChannel;
|
|
|
|
if (u == nil || c == nil || u.isLoggedIn == NO || c.isChannel == NO) {
|
|
return;
|
|
}
|
|
|
|
[u createChannelQuietListSheet];
|
|
|
|
[u sendModes:@"+q" withParameters:nil inChannel:c];
|
|
}
|
|
|
|
- (void)toggleChannelModerationMode:(id)sender
|
|
{
|
|
NSParameterAssert(sender != nil);
|
|
|
|
IRCClient *u = self.selectedClient;
|
|
IRCChannel *c = self.selectedChannel;
|
|
|
|
if (u == nil || c == nil || u.isLoggedIn == NO || c.isChannel == NO) {
|
|
return;
|
|
}
|
|
|
|
NSString *modeSymbol = nil;
|
|
|
|
if ([sender tag] == MTMMChannelModesMenuRemoveModerated) {
|
|
modeSymbol = @"-m";
|
|
} else {
|
|
modeSymbol = @"+m";
|
|
}
|
|
|
|
[u sendModes:modeSymbol withParameters:nil inChannel:c];
|
|
}
|
|
|
|
- (void)toggleChannelInviteMode:(id)sender
|
|
{
|
|
NSParameterAssert(sender != nil);
|
|
|
|
IRCClient *u = self.selectedClient;
|
|
IRCChannel *c = self.selectedChannel;
|
|
|
|
if (u == nil || c == nil || u.isLoggedIn == NO || c.isChannel == NO) {
|
|
return;
|
|
}
|
|
|
|
NSString *modeSymbol = nil;
|
|
|
|
if ([sender tag] == MTMMChannelModesMenuRemoveInviteOnly) {
|
|
modeSymbol = @"-i";
|
|
} else {
|
|
modeSymbol = @"+i";
|
|
}
|
|
|
|
[u sendModes:modeSymbol withParameters:nil inChannel:c];
|
|
}
|
|
|
|
#pragma mark -
|
|
#pragma mark Window
|
|
|
|
- (void)closeWindow:(id)sender
|
|
{
|
|
TXCommandWKeyAction keyAction = [TPCPreferences commandWKeyAction];
|
|
|
|
if (keyAction == TXCommandWKeyActionCloseWindow || mainWindow().keyWindow == NO) {
|
|
NSWindow *windowToClose = [NSApp keyWindow];
|
|
|
|
if (windowToClose == nil) {
|
|
windowToClose = [NSApp mainWindow];
|
|
}
|
|
|
|
if (windowToClose) {
|
|
[windowToClose performClose:sender];
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
IRCClient *u = self.selectedClient;
|
|
IRCChannel *c = self.selectedChannel;
|
|
|
|
if (u == nil) {
|
|
return;
|
|
}
|
|
|
|
switch (keyAction) {
|
|
case TXCommandWKeyActionPartChannel:
|
|
{
|
|
if (c == nil) {
|
|
return;
|
|
}
|
|
|
|
if (c.isChannel) {
|
|
if (c.isActive == NO) {
|
|
return;
|
|
}
|
|
|
|
[u partChannel:c];
|
|
} else {
|
|
[worldController() destroyChannel:c];
|
|
}
|
|
|
|
break;
|
|
}
|
|
case TXCommandWKeyActionDisconnect:
|
|
{
|
|
if (u.isConnecting == NO && u.isConnected == NO) {
|
|
return;
|
|
}
|
|
|
|
[u quit];
|
|
|
|
break;
|
|
}
|
|
case TXCommandWKeyActionTerminate:
|
|
{
|
|
[NSApp terminate:sender];
|
|
|
|
break;
|
|
}
|
|
default:
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
- (void)showMainWindow:(id)sender
|
|
{
|
|
[mainWindow() makeKeyAndOrderFront:sender];
|
|
}
|
|
|
|
- (void)centerMainWindow:(id)sender
|
|
{
|
|
[mainWindow() exactlyCenterWindow];
|
|
}
|
|
|
|
- (void)toggleFullscreen:(id)sender
|
|
{
|
|
[[NSApp keyWindow] toggleFullScreen:sender];
|
|
}
|
|
|
|
- (void)resetMainWindowFrame:(id)sender
|
|
{
|
|
if (mainWindow().inFullscreenMode) {
|
|
[mainWindow() toggleFullScreen:sender];
|
|
}
|
|
|
|
[mainWindow() setFrame:[mainWindow() defaultWindowFrame] display:YES animate:YES];
|
|
|
|
[mainWindow() exactlyCenterWindow];
|
|
}
|
|
|
|
- (void)sortChannelListNames:(id)sender
|
|
{
|
|
for (IRCClient *u in worldController().clientList) {
|
|
NSMutableArray *channelList = [u.channelList mutableCopy];
|
|
|
|
[channelList sortUsingComparator:^NSComparisonResult(IRCChannel *channel1, IRCChannel *channel2) {
|
|
if (channel1.isChannel && channel2.isChannel == NO) {
|
|
return NSOrderedAscending;
|
|
}
|
|
|
|
NSString *name1 = channel1.name.lowercaseString;
|
|
NSString *name2 = channel2.name.lowercaseString;
|
|
|
|
return [name1 compare:name2];
|
|
}];
|
|
|
|
if ([channelList isEqualToArray:u.channelList]) {
|
|
continue;
|
|
}
|
|
|
|
u.channelList = channelList;
|
|
|
|
[u reloadServerListItems];
|
|
}
|
|
|
|
[worldController() save];
|
|
}
|
|
|
|
- (void)markAllAsRead:(id)sender
|
|
{
|
|
[mainWindow() markAllAsRead];
|
|
}
|
|
|
|
#pragma mark -
|
|
#pragma mark Preferences
|
|
|
|
- (void)importPreferences:(id)sender
|
|
{
|
|
[TPCPreferencesImportExport importInWindow:mainWindow()];
|
|
}
|
|
|
|
- (void)exportPreferences:(id)sender
|
|
{
|
|
[TPCPreferencesImportExport exportInWindow:mainWindow()];
|
|
}
|
|
|
|
#pragma mark -
|
|
#pragma mark Off-the-Record Messaging
|
|
|
|
#if TEXTUAL_BUILT_WITH_ADVANCED_ENCRYPTION == 1
|
|
#define _encryptionNotEnabled ([TPCPreferences textEncryptionIsEnabled] == NO)
|
|
|
|
- (void)encryptionStartPrivateConversation:(id)sender
|
|
{
|
|
IRCClient *u = self.selectedClient;
|
|
IRCChannel *c = self.selectedChannel;
|
|
|
|
if (_encryptionNotEnabled || u == nil || c == nil || u.isLoggedIn == NO || c.isPrivateMessage == NO) {
|
|
return;
|
|
}
|
|
|
|
[sharedEncryptionManager() beginConversationWith:[u encryptionAccountNameForUser:c.name]
|
|
from:[u encryptionAccountNameForLocalUser]];
|
|
}
|
|
|
|
- (void)encryptionRefreshPrivateConversation:(id)sender
|
|
{
|
|
IRCClient *u = self.selectedClient;
|
|
IRCChannel *c = self.selectedChannel;
|
|
|
|
if (_encryptionNotEnabled || u == nil || c == nil || u.isLoggedIn == NO || c.isPrivateMessage == NO) {
|
|
return;
|
|
}
|
|
|
|
[sharedEncryptionManager() refreshConversationWith:[u encryptionAccountNameForUser:c.name]
|
|
from:[u encryptionAccountNameForLocalUser]];
|
|
}
|
|
|
|
- (void)encryptionEndPrivateConversation:(id)sender
|
|
{
|
|
IRCClient *u = self.selectedClient;
|
|
IRCChannel *c = self.selectedChannel;
|
|
|
|
if (_encryptionNotEnabled || u == nil || c == nil || u.isLoggedIn == NO || c.isPrivateMessage == NO) {
|
|
return;
|
|
}
|
|
|
|
[sharedEncryptionManager() endConversationWith:[u encryptionAccountNameForUser:c.name]
|
|
from:[u encryptionAccountNameForLocalUser]];
|
|
}
|
|
|
|
- (void)encryptionAuthenticateChatPartner:(id)sender
|
|
{
|
|
IRCClient *u = self.selectedClient;
|
|
IRCChannel *c = self.selectedChannel;
|
|
|
|
if (_encryptionNotEnabled || u == nil || c == nil || u.isLoggedIn == NO || c.isPrivateMessage == NO) {
|
|
return;
|
|
}
|
|
|
|
[sharedEncryptionManager() authenticateUser:[u encryptionAccountNameForUser:c.name]
|
|
from:[u encryptionAccountNameForLocalUser]];
|
|
}
|
|
|
|
- (void)encryptionListFingerprints:(id)sender
|
|
{
|
|
[sharedEncryptionManager() presentListOfFingerprints];
|
|
}
|
|
|
|
- (void)encryptionWhatIsThisInformation:(id)sender
|
|
{
|
|
[TLOpenLink openWithString:@"https://help.codeux.com/textual/Off-the-Record-Messaging.kb" inBackground:NO];
|
|
}
|
|
|
|
#undef _encryptionNotEnabled
|
|
#endif
|
|
|
|
#pragma mark -
|
|
#pragma mark Notifications
|
|
|
|
- (void)toggleMuteOnNotificationsShortcutOn:(BOOL)toggleOn
|
|
{
|
|
sharedNotificationController().areNotificationsDisabled = toggleOn;
|
|
|
|
NSControlStateValue state = ((toggleOn) ? NSControlStateValueOn : NSControlStateValueOff);
|
|
|
|
self.muteNotificationsFileMenuItem.state = state;
|
|
|
|
self.muteNotificationsDockMenuItem.state = state;
|
|
}
|
|
|
|
- (void)toggleMuteOnNotificationSoundsShortcutOn:(BOOL)toggleOn
|
|
{
|
|
[TPCPreferences setSoundIsMuted:toggleOn];
|
|
|
|
NSControlStateValue state = ((toggleOn) ? NSControlStateValueOn : NSControlStateValueOff);
|
|
|
|
self.muteNotificationsSoundsDockMenuItem.state = state;
|
|
|
|
self.muteNotificationsSoundsFileMenuItem.state = state;
|
|
}
|
|
|
|
- (void)toggleMuteOnNotificationSounds:(id)sender
|
|
{
|
|
if ([TPCPreferences soundIsMuted]) {
|
|
[self toggleMuteOnNotificationSoundsShortcutOn:NO];
|
|
} else {
|
|
[self toggleMuteOnNotificationSoundsShortcutOn:YES];
|
|
}
|
|
}
|
|
|
|
- (void)toggleMuteOnNotifications:(id)sender
|
|
{
|
|
if (sharedNotificationController().areNotificationsDisabled) {
|
|
[self toggleMuteOnNotificationsShortcutOn:NO];
|
|
} else {
|
|
[self toggleMuteOnNotificationsShortcutOn:YES];
|
|
}
|
|
}
|
|
|
|
#pragma mark -
|
|
#pragma mark Appearance
|
|
|
|
- (void)resetMainWindowAppearance:(id)sender
|
|
{
|
|
[TPCPreferences setAppearance:TXPreferredAppearanceInherited];
|
|
|
|
[TPCPreferences performReloadAction:TPCPreferencesReloadActionAppearance];
|
|
}
|
|
|
|
- (void)toggleMainWindowAppearance:(id)sender
|
|
{
|
|
TXPreferredAppearance appearance = [TPCPreferences appearance];
|
|
|
|
switch (appearance) {
|
|
case TXPreferredAppearanceInherited:
|
|
{
|
|
TXAppearance *appAppearance = [TXSharedApplication sharedAppearance];
|
|
|
|
if (appAppearance.properties.isDarkAppearance == NO) {
|
|
appearance = TXPreferredAppearanceDark;
|
|
} else {
|
|
appearance = TXPreferredAppearanceLight;
|
|
}
|
|
|
|
break;
|
|
}
|
|
case TXPreferredAppearanceLight:
|
|
{
|
|
appearance = TXPreferredAppearanceDark;
|
|
|
|
break;
|
|
}
|
|
case TXPreferredAppearanceDark:
|
|
{
|
|
appearance = TXPreferredAppearanceLight;
|
|
|
|
break;
|
|
}
|
|
} // switch()
|
|
|
|
[TPCPreferences setAppearance:appearance];
|
|
|
|
[TPCPreferences performReloadAction:TPCPreferencesReloadActionAppearance];
|
|
}
|
|
|
|
- (void)toggleServerListVisibility:(id)sender
|
|
{
|
|
[mainWindow().contentSplitView toggleServerListVisibility];
|
|
}
|
|
|
|
- (void)toggleMemberListVisibility:(id)sender
|
|
{
|
|
mainWindowMemberList().isHiddenByUser = (mainWindowMemberList().isHiddenByUser == NO);
|
|
|
|
[mainWindow().contentSplitView toggleMemberListVisibility];
|
|
}
|
|
|
|
- (void)forceReloadTheme:(id)sender
|
|
{
|
|
[mainWindow() reloadTheme];
|
|
}
|
|
|
|
#pragma mark -
|
|
#pragma mark License Manager
|
|
|
|
- (void)manageLicense:(id)sender
|
|
{
|
|
#if TEXTUAL_BUILT_WITH_LICENSE_MANAGER == 1
|
|
[self manageLicense:sender activateLicenseKey:nil licenseKeyPassedByArgument:NO];
|
|
#endif
|
|
}
|
|
|
|
#if TEXTUAL_BUILT_WITH_LICENSE_MANAGER == 1
|
|
- (void)manageLicense:(id)sender activateLicenseKey:(nullable NSString *)licenseKey
|
|
{
|
|
[self manageLicense:sender activateLicenseKey:licenseKey licenseKeyPassedByArgument:NO];
|
|
}
|
|
|
|
- (void)manageLicense:(id)sender activateLicenseKeyWithURL:(NSURL *)licenseKeyURL
|
|
{
|
|
NSParameterAssert(licenseKeyURL != nil);
|
|
|
|
NSString *path = licenseKeyURL.path;
|
|
|
|
if (path == nil) {
|
|
return;
|
|
}
|
|
|
|
NSCharacterSet *slashCharacterSet = [NSCharacterSet characterSetWithCharactersInString:@"/"];
|
|
|
|
NSString *licenseKey = [path stringByTrimmingCharactersInSet:slashCharacterSet];
|
|
|
|
if (licenseKey.length == 0) {
|
|
return;
|
|
}
|
|
|
|
[self manageLicense:sender activateLicenseKey:licenseKey licenseKeyPassedByArgument:NO];
|
|
}
|
|
|
|
- (void)manageLicense:(id)sender activateLicenseKey:(nullable NSString *)licenseKey licenseKeyPassedByArgument:(BOOL)licenseKeyPassedByArgument
|
|
{
|
|
TDCLicenseManagerDialog *licenseDialog = [TXSharedApplication sharedLicenseManagerDialog];
|
|
|
|
[licenseDialog show];
|
|
|
|
if (licenseKey) {
|
|
[licenseDialog activateLicenseKey:licenseKey silently:licenseKeyPassedByArgument];
|
|
}
|
|
}
|
|
#endif
|
|
|
|
#pragma mark -
|
|
#pragma mark Developer
|
|
|
|
- (void)toggleDeveloperMode:(id)sender
|
|
{
|
|
[TPCPreferences setDeveloperModeEnabled:([TPCPreferences developerModeEnabled] == NO)];
|
|
|
|
[TPCPreferences performReloadAction:TPCPreferencesReloadActionIRCCommandCache];
|
|
}
|
|
|
|
- (void)resetDoNotAskMePopupWarnings:(id)sender
|
|
{
|
|
NSDictionary *settings = [RZUserDefaults() dictionaryRepresentation];
|
|
|
|
for (NSString *key in settings) {
|
|
if ([key hasPrefix:TDCAlertSuppressionPrefix] == NO) {
|
|
continue;
|
|
}
|
|
|
|
[RZUserDefaults() setBool:NO forKey:key];
|
|
}
|
|
}
|
|
|
|
#pragma mark -
|
|
#pragma mark Sparkle Framework
|
|
|
|
- (void)checkForUpdates:(id)sender
|
|
{
|
|
#if TEXTUAL_BUILT_WITH_SPARKLE_ENABLED == 1
|
|
SPUStandardUpdaterController *controller = masterController().updateController;
|
|
|
|
[controller checkForUpdates:sender];
|
|
#endif
|
|
}
|
|
|
|
#pragma mark -
|
|
#pragma mark Navigation
|
|
|
|
- (void)navigateToTreeItemAtURL:(NSURL *)url
|
|
{
|
|
NSParameterAssert(url != nil);
|
|
|
|
NSString *path = url.path;
|
|
|
|
if (path == nil) {
|
|
return;
|
|
}
|
|
|
|
NSCharacterSet *slashCharacterSet = [NSCharacterSet characterSetWithCharactersInString:@"/"];
|
|
|
|
NSString *identifier = [path stringByTrimmingCharactersInSet:slashCharacterSet];
|
|
|
|
if (identifier.length == 0) {
|
|
return;
|
|
}
|
|
|
|
[self navigateToTreeItemWithIdentifier:identifier];
|
|
}
|
|
|
|
- (void)navigateToTreeItemWithIdentifier:(NSString *)identifier
|
|
{
|
|
NSParameterAssert(identifier != nil);
|
|
|
|
/* Do not use assert for this condition so we
|
|
don't crash user when we open a malformed URL. */
|
|
if (identifier.length != 36) {
|
|
return;
|
|
}
|
|
|
|
IRCTreeItem *item = [worldController() findItemWithId:identifier];
|
|
|
|
if (item == nil) {
|
|
return;
|
|
}
|
|
|
|
[self navigateToTreeItem:item];
|
|
}
|
|
|
|
- (void)navigateToTreeItem:(IRCTreeItem *)item
|
|
{
|
|
NSParameterAssert(item != nil);
|
|
|
|
[mainWindow() select:item];
|
|
}
|
|
|
|
- (void)populateNavigationChannelList
|
|
{
|
|
[self.mainMenuNavigationChannelListMenu removeAllItems];
|
|
|
|
NSUInteger channelCount = 0;
|
|
|
|
for (IRCClient *u in worldController().clientList) {
|
|
NSMenu *channelSubmenu = [NSMenu new];
|
|
|
|
NSMenuItem *clientMenuItem = [NSMenuItem new];
|
|
|
|
clientMenuItem.title = u.name;
|
|
|
|
clientMenuItem.submenu = channelSubmenu;
|
|
|
|
for (IRCChannel *c in u.channelList) {
|
|
NSMenuItem *channelMenuItem = nil;
|
|
|
|
if (channelCount >= 10) {
|
|
channelMenuItem = [NSMenuItem menuItemWithTitle:c.name
|
|
target:self
|
|
action:@selector(_navigateToChannelInNavigationList:)];
|
|
} else {
|
|
NSUInteger keyboardIndex = (channelCount + 1);
|
|
|
|
if (keyboardIndex == 10) {
|
|
keyboardIndex = 0; // Have 0 as the last item.
|
|
}
|
|
|
|
channelMenuItem = [NSMenuItem menuItemWithTitle:c.name
|
|
target:self
|
|
action:@selector(_navigateToChannelInNavigationList:)
|
|
keyEquivalent:[NSString stringWithUniChar:('0' + keyboardIndex)]
|
|
keyEquivalentMask:NSEventModifierFlagCommand];
|
|
}
|
|
|
|
channelMenuItem.userInfo = [worldController() pasteboardStringForItem:c];
|
|
|
|
[channelSubmenu addItem:channelMenuItem];
|
|
|
|
channelCount += 1;
|
|
}
|
|
|
|
[self.mainMenuNavigationChannelListMenu addItem:clientMenuItem];
|
|
}
|
|
}
|
|
|
|
- (void)_navigateToChannelInNavigationList:(NSMenuItem *)sender
|
|
{
|
|
IRCTreeItem *treeItem = [worldController() findItemWithPasteboardString:sender.userInfo];
|
|
|
|
if (treeItem == nil) {
|
|
return;
|
|
}
|
|
|
|
[mainWindow() select:treeItem];
|
|
}
|
|
|
|
- (void)performNavigationAction:(id)sender
|
|
{
|
|
NSParameterAssert(sender != nil);
|
|
|
|
IRCClient *u = self.selectedClient;
|
|
|
|
if (u == nil) {
|
|
return;
|
|
}
|
|
|
|
switch ([sender tag]) {
|
|
case MTMMNavigationServersMenuNextServer:
|
|
{
|
|
[mainWindow() selectNextServer:sender];
|
|
|
|
break;
|
|
}
|
|
case MTMMNavigationServersMenuPreviousServer:
|
|
{
|
|
[mainWindow() selectPreviousServer:sender];
|
|
|
|
break;
|
|
}
|
|
case MTMMNavigationServersMenuNextActiveServer:
|
|
{
|
|
[mainWindow() selectNextActiveServer:sender];
|
|
|
|
break;
|
|
}
|
|
case MTMMNavigationServersMenuPreviousActiveServer:
|
|
{
|
|
[mainWindow() selectPreviousActiveServer:sender];
|
|
|
|
break;
|
|
}
|
|
case MTMMNavigationChannelsMenuNextChannel:
|
|
{
|
|
[mainWindow() selectNextChannel:sender];
|
|
|
|
break;
|
|
}
|
|
case MTMMNavigationChannelsMenuPreviousChannel:
|
|
{
|
|
[mainWindow() selectPreviousChannel:sender];
|
|
|
|
break;
|
|
}
|
|
case MTMMNavigationChannelsMenuNextActiveChannel:
|
|
{
|
|
[mainWindow() selectNextActiveChannel:sender];
|
|
|
|
break;
|
|
}
|
|
case MTMMNavigationChannelsMenuPreviousActiveChannel:
|
|
{
|
|
[mainWindow() selectPreviousActiveChannel:sender];
|
|
|
|
break;
|
|
}
|
|
case MTMMNavigationChannelsMenuNextUnreadChannel:
|
|
{
|
|
[mainWindow() selectNextUnreadChannel:sender];
|
|
|
|
break;
|
|
}
|
|
case MTMMNavigationChannelsMenuPreviousUnreadChannel:
|
|
{
|
|
[mainWindow() selectPreviousUnreadChannel:sender];
|
|
|
|
break;
|
|
}
|
|
case MTMMNavigationMoveBackward:
|
|
{
|
|
[mainWindow() selectPreviousWindow:sender];
|
|
|
|
break;
|
|
}
|
|
case MTMMNavigationMoveForward:
|
|
{
|
|
[mainWindow() selectNextWindow:sender];
|
|
|
|
break;
|
|
}
|
|
case MTMMNavigationPreviousSelection:
|
|
{
|
|
[mainWindow() selectPreviousSelection:sender];
|
|
|
|
break;
|
|
}
|
|
} // switch()
|
|
}
|
|
|
|
- (void)onNextHighlight:(id)sender
|
|
{
|
|
TVCLogController *viewController = self.selectedViewController;
|
|
|
|
if (viewController == nil) {
|
|
return;
|
|
}
|
|
|
|
[viewController nextHighlight];
|
|
}
|
|
|
|
- (void)onPreviousHighlight:(id)sender
|
|
{
|
|
TVCLogController *viewController = self.selectedViewController;
|
|
|
|
if (viewController == nil) {
|
|
return;
|
|
}
|
|
|
|
[viewController previousHighlight];
|
|
}
|
|
|
|
- (void)jumpToCurrentSession:(id)sender
|
|
{
|
|
TVCLogController *viewController = self.selectedViewController;
|
|
|
|
if (viewController == nil) {
|
|
return;
|
|
}
|
|
|
|
[viewController jumpToCurrentSession];
|
|
}
|
|
|
|
- (void)jumpToPresent:(id)sender
|
|
{
|
|
TVCLogController *viewController = self.selectedViewController;
|
|
|
|
if (viewController == nil) {
|
|
return;
|
|
}
|
|
|
|
[viewController jumpToPresent];
|
|
}
|
|
|
|
#pragma mark -
|
|
#pragma mark Channel Properties Sheet
|
|
|
|
- (void)showChannelPropertiesSheet:(id)sender
|
|
{
|
|
[windowController() popMainWindowSheetIfExists];
|
|
|
|
IRCClient *u = self.selectedClient;
|
|
IRCChannel *c = self.selectedChannel;
|
|
|
|
if (u == nil || c == nil || c.isChannel == NO) {
|
|
return;
|
|
}
|
|
|
|
TDCChannelPropertiesSheet *sheet =
|
|
[[TDCChannelPropertiesSheet alloc] initWithChannel:c];
|
|
|
|
sheet.delegate = self;
|
|
|
|
sheet.window = mainWindow();
|
|
|
|
[sheet start];
|
|
|
|
[windowController() addWindowToWindowList:sheet];
|
|
}
|
|
|
|
- (void)channelPropertiesSheet:(TDCChannelPropertiesSheet *)sender onOk:(IRCChannelConfig *)config
|
|
{
|
|
IRCClient *u = sender.client;
|
|
|
|
if (u == nil) {
|
|
return;
|
|
}
|
|
|
|
IRCChannel *c = sender.channel;
|
|
|
|
if (c == nil) {
|
|
[worldController() createChannelWithConfig:config onClient:u];
|
|
|
|
[mainWindow() expandClient:u];
|
|
|
|
return;
|
|
}
|
|
|
|
[c updateConfig:config];
|
|
|
|
[worldController() save];
|
|
}
|
|
|
|
- (void)channelPropertiesSheetWillClose:(TDCChannelPropertiesSheet *)sender
|
|
{
|
|
[windowController() removeWindowFromWindowList:sender];
|
|
}
|
|
|
|
#pragma mark -
|
|
#pragma mark Channel Invite Sheet
|
|
|
|
- (void)memberSendInvite:(id)sender
|
|
{
|
|
[windowController() popMainWindowSheetIfExists];
|
|
|
|
IRCClient *u = self.selectedClient;
|
|
IRCChannel *c = self.selectedChannel;
|
|
|
|
if (u == nil || c == nil || u.isLoggedIn == NO || c.isChannel == NO || c.isActive == NO) {
|
|
return;
|
|
}
|
|
|
|
NSArray *nicknames = [self selectedMembersNicknames:sender];
|
|
|
|
if (nicknames.count == 0) {
|
|
return;
|
|
}
|
|
|
|
[self deselectMembers:sender];
|
|
|
|
NSMutableArray<NSString *> *channels = [NSMutableArray array];
|
|
|
|
for (IRCChannel *e in u.channelList) {
|
|
if (c != e && e.isChannel) {
|
|
[channels addObject:e.name];
|
|
}
|
|
}
|
|
|
|
if (channels.count == 0) {
|
|
return;
|
|
}
|
|
|
|
TDCChannelInviteSheet *sheet =
|
|
[[TDCChannelInviteSheet alloc] initWithNicknames:nicknames onClient:u];
|
|
|
|
sheet.delegate = (id)self;
|
|
|
|
sheet.window = mainWindow();
|
|
|
|
[sheet startWithChannels:channels];
|
|
|
|
[windowController() addWindowToWindowList:sheet];
|
|
}
|
|
|
|
- (void)channelInviteSheet:(TDCChannelInviteSheet *)sender onSelectChannel:(NSString *)channelName
|
|
{
|
|
IRCClient *u = sender.client;
|
|
|
|
if (u == nil || u.isLoggedIn == NO) {
|
|
return;
|
|
}
|
|
|
|
for (NSString *nickname in sender.nicknames) {
|
|
[u sendInviteTo:nickname toJoinChannelNamed:channelName];
|
|
}
|
|
}
|
|
|
|
- (void)channelInviteSheetWillClose:(TDCChannelInviteSheet *)sender
|
|
{
|
|
[windowController() removeWindowFromWindowList:sender];
|
|
}
|
|
|
|
#pragma mark -
|
|
#pragma mark Address Book Sheet
|
|
|
|
- (void)showAddressBook:(id)sender
|
|
{
|
|
IRCClient *u = self.selectedClient;
|
|
|
|
if (u == nil) {
|
|
return;
|
|
}
|
|
|
|
[self showServerPropertiesSheetForClient:u
|
|
withSelection:TDCServerPropertiesSheetSelectionAddressBook
|
|
context:nil];
|
|
}
|
|
|
|
- (void)showIgnoreList:(id)sender
|
|
{
|
|
[self showAddressBook:sender];
|
|
}
|
|
|
|
#pragma mark -
|
|
#pragma mark Welcome Sheet
|
|
|
|
- (void)showWelcomeSheet:(id)sender
|
|
{
|
|
[windowController() popMainWindowSheetIfExists];
|
|
|
|
TDCWelcomeSheet *sheet =
|
|
[[TDCWelcomeSheet alloc] initWithWindow:mainWindow()];
|
|
|
|
sheet.delegate = (id)self;
|
|
|
|
[sheet start];
|
|
|
|
[windowController() addWindowToWindowList:sheet];
|
|
}
|
|
|
|
- (void)welcomeSheet:(TDCWelcomeSheet *)sender onOk:(IRCClientConfig *)config
|
|
{
|
|
IRCClient *u = [worldController() createClientWithConfig:config reload:YES];
|
|
|
|
[mainWindow() expandClient:u];
|
|
|
|
[worldController() save];
|
|
|
|
[u connect];
|
|
|
|
[u selectFirstChannelInChannelList];
|
|
}
|
|
|
|
- (void)welcomeSheetWillClose:(TDCWelcomeSheet *)sender
|
|
{
|
|
[windowController() removeWindowFromWindowList:sender];
|
|
}
|
|
|
|
#pragma mark -
|
|
#pragma mark About Window
|
|
|
|
- (void)showAboutWindow:(id)sender
|
|
{
|
|
_popWindowViewIfExists(@"TDCAboutDialog");
|
|
|
|
TDCAboutDialog *dialog = [TDCAboutDialog new];
|
|
|
|
dialog.delegate = (id)self;
|
|
|
|
[dialog show];
|
|
|
|
[windowController() addWindowToWindowList:dialog];
|
|
}
|
|
|
|
- (void)aboutDialogWillClose:(TDCAboutDialog *)sender
|
|
{
|
|
[windowController() removeWindowFromWindowList:sender];
|
|
}
|
|
|
|
#pragma mark -
|
|
#pragma mark Server Properties Sheet
|
|
|
|
- (void)showServerPropertiesSheetForClient:(IRCClient *)client withSelection:(TDCServerPropertiesSheetSelection)selection context:(nullable id)context
|
|
{
|
|
NSParameterAssert(client != nil);
|
|
|
|
[windowController() popMainWindowSheetIfExists];
|
|
|
|
TDCServerPropertiesSheet *sheet = [[TDCServerPropertiesSheet alloc] initWithClient:client];
|
|
|
|
sheet.delegate = self;
|
|
|
|
sheet.window = mainWindow();
|
|
|
|
[sheet startWithSelection:selection context:context];
|
|
|
|
[windowController() addWindowToWindowList:sheet];
|
|
}
|
|
|
|
- (void)showServerPropertiesSheet:(id)sender
|
|
{
|
|
IRCClient *u = self.selectedClient;
|
|
|
|
if (u == nil) {
|
|
return;
|
|
}
|
|
|
|
[self showServerPropertiesSheetForClient:u
|
|
withSelection:TDCServerPropertiesSheetSelectionDefault
|
|
context:nil];
|
|
}
|
|
|
|
- (void)serverPropertiesSheet:(TDCServerPropertiesSheet *)sender onOk:(IRCClientConfig *)config
|
|
{
|
|
IRCClient *u = sender.client;
|
|
|
|
if (u == nil) {
|
|
u = [worldController() createClientWithConfig:config reload:YES];
|
|
|
|
[mainWindow() expandClient:u];
|
|
|
|
[worldController() save];
|
|
|
|
return;
|
|
}
|
|
|
|
BOOL sameEncoding = (config.primaryEncoding == u.config.primaryEncoding);
|
|
|
|
[u updateConfig:config];
|
|
|
|
if (sameEncoding == NO) {
|
|
[mainWindow() reloadTheme];
|
|
}
|
|
|
|
[mainWindow() reloadTreeGroup:u];
|
|
|
|
[worldController() save];
|
|
}
|
|
|
|
- (void)serverPropertiesSheetWillClose:(TDCServerPropertiesSheet *)sender
|
|
{
|
|
[windowController() removeWindowFromWindowList:sender];
|
|
}
|
|
|
|
#pragma mark -
|
|
#pragma mark Highlight List Sheet
|
|
|
|
- (void)showServerHighlightList:(id)sender
|
|
{
|
|
[windowController() popMainWindowSheetIfExists];
|
|
|
|
IRCClient *u = self.selectedClient;
|
|
|
|
if (u == nil) {
|
|
return;
|
|
}
|
|
|
|
TDCServerHighlightListSheet *sheet =
|
|
[[TDCServerHighlightListSheet alloc] initWithClient:u];
|
|
|
|
sheet.delegate = (id)self;
|
|
|
|
sheet.window = mainWindow();
|
|
|
|
[sheet start];
|
|
|
|
[windowController() addWindowToWindowList:sheet];
|
|
}
|
|
|
|
- (void)serverHighlightListSheetWillClose:(TDCServerHighlightListSheet *)sender
|
|
{
|
|
[windowController() removeWindowFromWindowList:sender];
|
|
}
|
|
|
|
#pragma mark -
|
|
#pragma mark Nickname Color Sheet
|
|
|
|
- (void)memberChangeColor:(NSString *)nickname
|
|
{
|
|
NSParameterAssert(nickname != nil);
|
|
|
|
[windowController() popMainWindowSheetIfExists];
|
|
|
|
IRCClient *u = self.selectedClient;
|
|
|
|
if (u == nil) {
|
|
return;
|
|
}
|
|
|
|
TDCNicknameColorSheet *sheet =
|
|
[[TDCNicknameColorSheet alloc] initWithNickname:nickname];
|
|
|
|
sheet.delegate = (id)self;
|
|
|
|
sheet.window = mainWindow();
|
|
|
|
[sheet start];
|
|
|
|
[windowController() addWindowToWindowList:sheet];
|
|
}
|
|
|
|
- (void)nicknameColorSheetOnOk:(TDCNicknameColorSheet *)sender
|
|
{
|
|
[mainWindow() reloadTheme];
|
|
}
|
|
|
|
- (void)nicknameColorSheetWillClose:(TDCNicknameColorSheet *)sender
|
|
{
|
|
[windowController() removeWindowFromWindowList:sender];
|
|
}
|
|
|
|
#pragma mark -
|
|
#pragma mark Channel Topic Sheet
|
|
|
|
- (void)showChannelModifyTopicSheet:(id)sender
|
|
{
|
|
[windowController() popMainWindowSheetIfExists];
|
|
|
|
IRCClient *u = self.selectedClient;
|
|
IRCChannel *c = self.selectedChannel;
|
|
|
|
if (u == nil || c == nil || c.isChannel == NO) {
|
|
return;
|
|
}
|
|
|
|
TDCChannelModifyTopicSheet *sheet =
|
|
[[TDCChannelModifyTopicSheet alloc] initWithChannel:c];
|
|
|
|
sheet.delegate = (id)self;
|
|
|
|
sheet.window = mainWindow();
|
|
|
|
[sheet start];
|
|
|
|
[windowController() addWindowToWindowList:sheet];
|
|
}
|
|
|
|
- (void)channelModifyTopicSheet:(TDCChannelModifyTopicSheet *)sender onOk:(NSString *)topic
|
|
{
|
|
IRCClient *u = sender.client;
|
|
IRCChannel *c = sender.channel;
|
|
|
|
if (u == nil || c == nil || u.isLoggedIn == NO || c.isChannel == NO) {
|
|
return;
|
|
}
|
|
|
|
[u sendTopicTo:topic inChannel:c];
|
|
}
|
|
|
|
- (void)channelModifyTopicSheetWillClose:(TDCChannelModifyTopicSheet *)sender
|
|
{
|
|
[windowController() removeWindowFromWindowList:sender];
|
|
}
|
|
|
|
#pragma mark -
|
|
#pragma mark Channel Mode Sheet
|
|
|
|
- (void)showChannelModifyModesSheet:(id)sender
|
|
{
|
|
[windowController() popMainWindowSheetIfExists];
|
|
|
|
IRCClient *u = self.selectedClient;
|
|
IRCChannel *c = self.selectedChannel;
|
|
|
|
if (u == nil || c == nil || c.isChannel == NO) {
|
|
return;
|
|
}
|
|
|
|
TDCChannelModifyModesSheet *sheet =
|
|
[[TDCChannelModifyModesSheet alloc] initWithChannel:c];
|
|
|
|
sheet.delegate = (id)self;
|
|
|
|
sheet.window = mainWindow();
|
|
|
|
[sheet start];
|
|
|
|
[windowController() addWindowToWindowList:sheet];
|
|
}
|
|
|
|
- (void)channelModifyModesSheet:(TDCChannelModifyModesSheet *)sender onOk:(IRCChannelModeContainer *)modes
|
|
{
|
|
IRCClient *u = sender.client;
|
|
IRCChannel *c = sender.channel;
|
|
|
|
if (u == nil || c == nil || u.isLoggedIn == NO || c.isChannel == NO) {
|
|
return;
|
|
}
|
|
|
|
NSString *changeString = [c.modeInfo getChangeCommand:modes];
|
|
|
|
if (changeString.length == 0) {
|
|
return;
|
|
}
|
|
|
|
[u sendModes:changeString withParameters:nil inChannel:c];
|
|
}
|
|
|
|
- (void)channelModifyModesSheetWillClose:(TDCChannelModifyModesSheet *)sender
|
|
{
|
|
[windowController() removeWindowFromWindowList:sender];
|
|
}
|
|
|
|
#pragma mark -
|
|
#pragma mark Channel Spotlight Window
|
|
|
|
- (void)showChannelSpotlightWindow:(id)sender
|
|
{
|
|
_popWindowViewIfExists(@"TDCChannelSpotlightController");
|
|
|
|
TDCChannelSpotlightController *dialog = [TDCChannelSpotlightController new];
|
|
|
|
dialog.delegate = (id)self;
|
|
|
|
[dialog show];
|
|
|
|
[windowController() addWindowToWindowList:dialog];
|
|
}
|
|
|
|
- (void)channelSpotlightController:(TDCChannelSpotlightController *)sender selectChannel:(IRCChannel *)channel
|
|
{
|
|
[mainWindow() select:channel];
|
|
}
|
|
|
|
- (void)channelSpotlightControllerWillClose:(TDCChannelSpotlightController *)sender
|
|
{
|
|
[windowController() removeWindowFromWindowList:sender];
|
|
}
|
|
|
|
#pragma mark -
|
|
#pragma mark Change Nickname Sheet
|
|
|
|
- (void)showServerChangeNicknameSheet:(id)sender
|
|
{
|
|
[windowController() popMainWindowSheetIfExists];
|
|
|
|
IRCClient *u = self.selectedClient;
|
|
|
|
if (u == nil || u.isLoggedIn == NO) {
|
|
return;
|
|
}
|
|
|
|
TDCServerChangeNicknameSheet *sheet =
|
|
[[TDCServerChangeNicknameSheet alloc] initWithClient:u];
|
|
|
|
sheet.delegate = (id)self;
|
|
|
|
sheet.window = mainWindow();
|
|
|
|
[sheet start];
|
|
|
|
[windowController() addWindowToWindowList:sheet];
|
|
}
|
|
|
|
- (void)serverChangeNicknameSheet:(TDCServerChangeNicknameSheet *)sender didInputNickname:(NSString *)nickname
|
|
{
|
|
IRCClient *u = sender.client;
|
|
|
|
if (u == nil || u.isConnected == NO) {
|
|
return;
|
|
}
|
|
|
|
[u changeNickname:nickname];
|
|
}
|
|
|
|
- (void)serverChangeNicknameSheetWillClose:(TDCServerChangeNicknameSheet *)sender
|
|
{
|
|
[windowController() removeWindowFromWindowList:sender];
|
|
}
|
|
|
|
#pragma mark -
|
|
#pragma mark Preferences Dialog
|
|
|
|
- (void)showPreferencesWindow:(id)sender
|
|
{
|
|
[self showPreferencesWindowWithSelection:TDCPreferencesControllerSelectionDefault];
|
|
}
|
|
|
|
- (void)showNotificationPreferences:(id)sender
|
|
{
|
|
[self showPreferencesWindowWithSelection:TDCPreferencesControllerSelectionNotifications];
|
|
}
|
|
|
|
- (void)showStylePreferences:(id)sender
|
|
{
|
|
[self showPreferencesWindowWithSelection:TDCPreferencesControllerSelectionStyle];
|
|
}
|
|
|
|
- (void)showHiddenPreferences:(id)sender
|
|
{
|
|
[self showPreferencesWindowWithSelection:TDCPreferencesControllerSelectionHiddenPreferences];
|
|
}
|
|
|
|
- (void)showPreferencesWindowWithSelection:(TDCPreferencesControllerSelection)selection
|
|
{
|
|
TDCPreferencesController *openWindow = [windowController() windowFromWindowList:@"TDCPreferencesController"];
|
|
|
|
if (openWindow) {
|
|
[openWindow show:selection];
|
|
|
|
return;
|
|
}
|
|
|
|
TDCPreferencesController *controller =
|
|
[TDCPreferencesController new];
|
|
|
|
controller.delegate = (id)self;
|
|
|
|
[controller show:selection];
|
|
|
|
[windowController() addWindowToWindowList:controller];
|
|
}
|
|
|
|
- (void)preferencesDialogWillClose:(TDCPreferencesController *)sender
|
|
{
|
|
[TPCPreferences performReloadAction:(TPCPreferencesReloadActionHighlightKeywords |
|
|
TPCPreferencesReloadActionPreferencesChanged)];
|
|
|
|
[windowController() removeWindowFromWindowList:sender];
|
|
}
|
|
|
|
@end
|
|
|
|
#pragma mark -
|
|
#pragma mark Main Window Proxy
|
|
|
|
@implementation TXMenuControllerMainWindowProxy
|
|
|
|
- (void)openStandaloneStoreWebpage:(id)sender
|
|
{
|
|
[menuController() openStandaloneStoreWebpage:sender];
|
|
}
|
|
|
|
- (void)manageLicense:(id)sender
|
|
{
|
|
[menuController() manageLicense:sender];
|
|
}
|
|
|
|
- (void)showWelcomeSheet:(id)sender
|
|
{
|
|
[menuController() showWelcomeSheet:sender];
|
|
}
|
|
|
|
@end
|
|
|
|
NS_ASSUME_NONNULL_END
|